From f6ff24f73bb4fb0f048fd8aeda4133dafbfcd5f3 Mon Sep 17 00:00:00 2001 From: alirahimi818 Date: Fri, 5 Jun 2026 21:27:38 +0200 Subject: [PATCH 1/3] Add Linux/macOS deployer script (Deploy-Fastly-Linux.sh) Bash equivalent of Deploy-Fastly-Windows.ps1 for Linux and macOS users. Requires: bash, curl, python3, openssl, npm. --- Deploy-Fastly-Linux.sh | 1314 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1314 insertions(+) create mode 100755 Deploy-Fastly-Linux.sh diff --git a/Deploy-Fastly-Linux.sh b/Deploy-Fastly-Linux.sh new file mode 100755 index 0000000..fbf12f6 --- /dev/null +++ b/Deploy-Fastly-Linux.sh @@ -0,0 +1,1314 @@ +#!/usr/bin/env bash +# XHTTPRelayECO >> Fastly Compute Deployer (Linux/macOS) +# by @b3hnamrjd +# Telegram : https://t.me/B3hnamR +# GitHub : https://github.com/B3hnamR + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NPM_EXE="npm" + +# ───────────────────────────────────────────── +# Colors +# ───────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +DARK_GRAY='\033[1;30m' +DARK_YELLOW='\033[0;33m' +NC='\033[0m' + +# ───────────────────────────────────────────── +# UI helpers +# ───────────────────────────────────────────── +write_banner() { + clear + echo -e "${CYAN}==============================================${NC}" + echo -e "${CYAN} XHTTPRelayECO >> Fastly Compute Deployer ${NC}" + echo -e "${CYAN} by @b3hnamrjd ${NC}" + echo -e "${CYAN} Telegram : https://t.me/B3hnamR ${NC}" + echo -e "${CYAN} GitHub : https://github.com/B3hnamR ${NC}" + echo -e "${CYAN}==============================================${NC}" + echo "" +} + +write_step() { echo ""; echo -e "${YELLOW}>> $1${NC}"; } +write_ok() { echo -e "${GREEN} $1${NC}"; } +write_info() { echo -e "${CYAN} $1${NC}"; } +write_warn() { echo -e "${DARK_YELLOW} $1${NC}"; } +write_err() { echo -e "${RED} $1${NC}"; } + +read_default() { + local prompt="$1" + local default_val="$2" + local result + read -rp "$prompt [$default_val]: " result + if [[ -z "${result// }" ]]; then + echo "$default_val" + else + echo "${result//[[:space:]]/}" + fi +} + +read_required() { + local prompt="$1" + local result + while true; do + read -rp "$prompt: " result + result="${result#"${result%%[![:space:]]*}"}" + result="${result%"${result##*[![:space:]]}"}" + if [[ -n "$result" ]]; then + echo "$result" + return + fi + write_err "Required - please enter a value." + done +} + +read_yes_no() { + local prompt="$1" + local default_yes="${2:-true}" + local def_str + if [[ "$default_yes" == "true" ]]; then def_str="Y/n"; else def_str="y/N"; fi + local v + while true; do + read -rp "$prompt ($def_str): " v + v=$(echo "$v" | tr '[:upper:]' '[:lower:]' | xargs) + if [[ -z "$v" ]]; then + echo "$default_yes"; return + fi + if [[ "$v" == "y" || "$v" == "yes" ]]; then echo "true"; return; fi + if [[ "$v" == "n" || "$v" == "no" ]]; then echo "false"; return; fi + write_err "Please enter y or n." + done +} + +normalize_path() { + local p="${1// /}" + if [[ -z "$p" ]]; then echo "/api"; return; fi + if [[ "${p:0:1}" != "/" ]]; then p="/$p"; fi + if [[ "${#p}" -gt 1 && "${p: -1}" == "/" ]]; then p="${p%/}"; fi + echo "$p" +} + +# ───────────────────────────────────────────── +# Token store (openssl AES-256-CBC) +# ───────────────────────────────────────────── +TOKEN_STORE="$SCRIPT_DIR/.fastly-token.enc" +_enc_pass="fastly-relay-$(id -u)-$(hostname)" + +save_token_secure() { + local token="$1" + echo "$token" | openssl enc -aes-256-cbc -pbkdf2 -pass "pass:$_enc_pass" -base64 2>/dev/null > "$TOKEN_STORE" +} + +load_token_secure() { + if [[ ! -f "$TOKEN_STORE" ]]; then echo ""; return; fi + openssl enc -aes-256-cbc -d -pbkdf2 -pass "pass:$_enc_pass" -base64 -in "$TOKEN_STORE" 2>/dev/null | tr -d '\n' || echo "" +} + +# ───────────────────────────────────────────── +# Project state store +# ───────────────────────────────────────────── +STATE_FILE="$SCRIPT_DIR/.fastly-deploy-state.json" + +load_deploy_state() { + if [[ ! -f "$STATE_FILE" ]]; then echo "[]"; return; fi + local raw + raw=$(cat "$STATE_FILE" 2>/dev/null || echo "") + if [[ -z "$raw" ]]; then echo "[]"; return; fi + echo "$raw" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get('services',[])))" 2>/dev/null || echo "[]" +} + +save_deploy_state() { + local services_json="$1" + local sorted + sorted=$(echo "$services_json" | python3 -c " +import sys,json +svcs=json.load(sys.stdin) +svcs.sort(key=lambda x: x.get('Name','')) +print(json.dumps({'services': svcs}, indent=2)) +" 2>/dev/null || echo '{"services":[]}') + echo "$sorted" > "$STATE_FILE" +} + +add_deploy_state_entry() { + local name="$1" service_id="$2" domain="$3" target_domain="$4" relay_path="$5" + local connect_timeout="${6:-10000}" first_byte="${7:-300000}" between_bytes="${8:-300000}" + local deployed_at + deployed_at=$(date '+%Y-%m-%d %H:%M:%S') + local existing + existing=$(load_deploy_state) + local updated + updated=$(echo "$existing" | python3 -c " +import sys,json +svcs=json.load(sys.stdin) +svcs=[s for s in svcs if s.get('Name') != '$name'] +svcs.append({ + 'Name': '$name', 'ServiceId': '$service_id', 'Domain': '$domain', + 'TargetDomain': '$target_domain', 'RelayPath': '$relay_path', + 'ConnectTimeout': $connect_timeout, 'FirstByteTimeout': $first_byte, + 'BetweenBytesTimeout': $between_bytes, 'DeployedAt': '$deployed_at' +}) +print(json.dumps(svcs)) +" 2>/dev/null || echo "[]") + save_deploy_state "$updated" +} + +get_saved_state_for_service() { + local service_id="$1" + load_deploy_state | python3 -c " +import sys,json +svcs=json.load(sys.stdin) +match=[s for s in svcs if s.get('ServiceId')=='$service_id'] +print(json.dumps(match[0]) if match else 'null') +" 2>/dev/null || echo "null" +} + +# ───────────────────────────────────────────── +# Fastly API helpers +# ───────────────────────────────────────────── +fastly_api() { + local method="$1" path="$2" token="$3" + local body="${4:-}" + local content_type="${5:-application/json}" + local uri="https://api.fastly.com$path" + local response http_code + + if [[ -n "$body" ]]; then + response=$(curl -s -w "\n%{http_code}" -X "$method" "$uri" \ + -H "Fastly-Key: $token" \ + -H "Accept: application/json" \ + -H "Content-Type: $content_type" \ + --data "$body" \ + --max-time 30 2>&1) + else + response=$(curl -s -w "\n%{http_code}" -X "$method" "$uri" \ + -H "Fastly-Key: $token" \ + -H "Accept: application/json" \ + --max-time 30 2>&1) + fi + + http_code=$(echo "$response" | tail -1) + body_out=$(echo "$response" | sed '$d') + + if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then + local detail + detail=$(echo "$body_out" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('detail',''))" 2>/dev/null || echo "") + if [[ -n "$detail" ]]; then + echo "Fastly API error ($method $path): $detail" >&2 + else + echo "Fastly API error ($method $path): HTTP $http_code" >&2 + fi + return 1 + fi + echo "$body_out" +} + +test_fastly_token() { + local token="$1" + local result + result=$(fastly_api GET /tokens/self "$token" 2>/dev/null) || return 1 + local id + id=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || echo "") + [[ -n "$id" ]] +} + +get_fastly_services() { + local token="$1" + local result + result=$(fastly_api GET "/service?per_page=100" "$token" 2>/dev/null) || { echo "[]"; return; } + echo "$result" | python3 -c " +import sys,json +svcs=json.load(sys.stdin) +if not isinstance(svcs, list): svcs=[] +print(json.dumps([s for s in svcs if s.get('type')=='wasm'])) +" 2>/dev/null || echo "[]" +} + +new_fastly_service() { + local token="$1" name="$2" + fastly_api POST /service "$token" "{\"name\":\"$name\",\"type\":\"wasm\"}" +} + +new_fastly_service_version() { + local token="$1" service_id="$2" + fastly_api POST "/service/$service_id/version" "$token" +} + +add_fastly_domain() { + local token="$1" service_id="$2" version="$3" domain_name="$4" + fastly_api POST "/service/$service_id/version/$version/domain" "$token" "{\"name\":\"$domain_name\"}" +} + +add_fastly_backend() { + local token="$1" service_id="$2" version="$3" hostname="$4" port="$5" + local connect_timeout="${6:-10000}" first_byte="${7:-300000}" between_bytes="${8:-300000}" + local uri="https://api.fastly.com/service/$service_id/version/$version/backend" + local form="name=origin_xhttp&address=$hostname&port=$port&use_ssl=1&ssl_check_cert=1" + form+="&ssl_sni_hostname=$hostname&ssl_cert_hostname=$hostname&override_host=$hostname" + form+="&connect_timeout=$connect_timeout&first_byte_timeout=$first_byte&between_bytes_timeout=$between_bytes" + curl -s -X POST "$uri" \ + -H "Fastly-Key: $token" \ + -H "Accept: application/json" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data "$form" \ + --max-time 30 > /dev/null 2>&1 +} + +validate_fastly_version() { + local token="$1" service_id="$2" version="$3" + fastly_api GET "/service/$service_id/version/$version/validate" "$token" +} + +activate_fastly_version() { + local token="$1" service_id="$2" version="$3" + fastly_api PUT "/service/$service_id/version/$version/activate" "$token" +} + +upload_fastly_package() { + local token="$1" service_id="$2" version="$3" pkg_path="$4" + local uri="https://api.fastly.com/service/$service_id/version/$version/package" + curl -s -X PUT "$uri" \ + -H "Fastly-Key: $token" \ + -H "Accept: application/json" \ + -F "package=@$pkg_path" \ + --max-time 300 > /dev/null 2>&1 +} + +get_fastly_active_version() { + local token="$1" service_id="$2" + local result + result=$(fastly_api GET "/service/$service_id/details" "$token" 2>/dev/null) || { echo 0; return; } + echo "$result" | python3 -c " +import sys,json +d=json.load(sys.stdin) +av=d.get('active_version') +print(av.get('number',0) if av else 0) +" 2>/dev/null || echo 0 +} + +# ───────────────────────────────────────────── +# Config Store helpers +# ───────────────────────────────────────────── +new_fastly_config_store() { + local token="$1" name="$2" + local result + result=$(fastly_api POST /resources/stores/config "$token" "{\"name\":\"$name\"}" 2>/dev/null) || { + # May already exist - try to find it + local stores + stores=$(get_fastly_config_stores "$token") + echo "$stores" | python3 -c " +import sys,json +stores=json.load(sys.stdin) +match=[s for s in stores if s.get('name')=='$name'] +print(json.dumps(match[0]) if match else 'null') +" 2>/dev/null || echo "null" + return + } + echo "$result" +} + +get_fastly_config_stores() { + local token="$1" + local result + result=$(fastly_api GET /resources/stores/config "$token" 2>/dev/null) || { echo "[]"; return; } + echo "$result" | python3 -c " +import sys,json +d=json.load(sys.stdin) +if isinstance(d, list): print(json.dumps(d)) +elif isinstance(d, dict): print(json.dumps(d.get('data',[]))) +else: print('[]') +" 2>/dev/null || echo "[]" +} + +set_fastly_config_store_item() { + local token="$1" store_id="$2" key="$3" value="$4" + local uri="https://api.fastly.com/resources/stores/config/$store_id/item/$key" + local body + body=$(python3 -c "import json; print(json.dumps({'item_value': '$value'}))" 2>/dev/null || echo "{\"item_value\":\"$value\"}") + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$uri" \ + -H "Fastly-Key: $token" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + --data "$body" \ + --max-time 15) + if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then + # Try POST if PUT failed + uri="https://api.fastly.com/resources/stores/config/$store_id" + body=$(python3 -c "import json; print(json.dumps({'item_key': '$key', 'item_value': '$value'}))" 2>/dev/null || echo "{\"item_key\":\"$key\",\"item_value\":\"$value\"}") + curl -s -o /dev/null -X POST "$uri" \ + -H "Fastly-Key: $token" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + --data "$body" \ + --max-time 15 || true + fi +} + +link_config_store_to_service() { + local token="$1" service_id="$2" version="$3" store_id="$4" + local uri="https://api.fastly.com/service/$service_id/version/$version/resource" + curl -s -o /dev/null -X POST "$uri" \ + -H "Fastly-Key: $token" \ + -H "Accept: application/json" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data "resource_id=$store_id&name=relay_config" \ + --max-time 15 2>/dev/null || write_warn "Config Store link note: could not link (may already be linked)." +} + +push_config_store() { + local token="$1" service_id="$2" version="$3" + local service_name="$4" target_domain="$5" hostname="$6" relay_path="$7" + write_step "Creating Config Store (relay ENV variables)..." + local store_name="relay_config_${service_name}" + local store + store=$(new_fastly_config_store "$token" "$store_name") + local store_id + store_id=$(echo "$store" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || echo "") + if [[ -z "$store_id" ]]; then + write_warn "Could not create Config Store - values will use fallback defaults." + return + fi + write_ok "Config Store: $store_name ($store_id)" + set_fastly_config_store_item "$token" "$store_id" "TARGET_BASE" "$target_domain" + set_fastly_config_store_item "$token" "$store_id" "TARGET_HOSTNAME" "$hostname" + set_fastly_config_store_item "$token" "$store_id" "RELAY_PATH" "$relay_path" + write_ok "Config Store values set: TARGET_BASE, TARGET_HOSTNAME, RELAY_PATH" + link_config_store_to_service "$token" "$service_id" "$version" "$store_id" + write_ok "Config Store linked to service version $version." +} + +# ───────────────────────────────────────────── +# Node / npm / js-compute-runtime helpers +# ───────────────────────────────────────────── +ensure_node() { + if command -v npm &>/dev/null; then + write_ok "npm already installed." + return + fi + write_step "npm not found. Attempting to install Node.js..." + if command -v apt-get &>/dev/null; then + write_info "Detected apt. Installing Node.js LTS via NodeSource..." + curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - 2>/dev/null + sudo apt-get install -y nodejs 2>/dev/null + elif command -v brew &>/dev/null; then + write_info "Detected Homebrew. Installing Node.js LTS..." + brew install node 2>/dev/null + elif command -v dnf &>/dev/null; then + write_info "Detected dnf. Installing Node.js LTS..." + sudo dnf install -y nodejs 2>/dev/null + else + write_err "Could not auto-install Node.js. Please install Node.js LTS manually." + write_err "Visit: https://nodejs.org/en/download/" + exit 1 + fi + if ! command -v npm &>/dev/null; then + write_err "Node.js installed but npm not detected. Please open a new terminal and retry." + exit 1 + fi +} + +ensure_js_compute_runtime() { + write_step "Checking @fastly/js-compute..." + cd "$SCRIPT_DIR" + if [[ ! -f "node_modules/.bin/js-compute-runtime" ]]; then + write_warn "js-compute-runtime not found. Running npm install..." + npm install + else + write_ok "js-compute-runtime found." + fi +} + +# ───────────────────────────────────────────── +# Build +# ───────────────────────────────────────────── +invoke_build() { + write_step "Building Wasm package..." + cd "$SCRIPT_DIR" + if ! npm run build; then + write_err "Build failed." + exit 1 + fi + write_ok "Build succeeded." +} + +# ───────────────────────────────────────────── +# Package tar.gz builder +# ───────────────────────────────────────────── +build_package() { + local service_name="$1" + write_step "Packaging Wasm..." + local pkg_dir="$SCRIPT_DIR/pkg" + mkdir -p "$pkg_dir" + local tmp_dir="$pkg_dir/$service_name" + rm -rf "$tmp_dir" + mkdir -p "$tmp_dir/bin" + cp "$SCRIPT_DIR/bin/main.wasm" "$tmp_dir/bin/main.wasm" + cp "$SCRIPT_DIR/fastly.toml" "$tmp_dir/fastly.toml" + local stamp out_tar + stamp=$(date '+%Y%m%d%H%M%S') + out_tar="$pkg_dir/deploy_${stamp}.tar.gz" + (cd "$pkg_dir" && tar -czf "deploy_${stamp}.tar.gz" "$service_name") + rm -rf "$tmp_dir" + write_ok "Package: $out_tar" + echo "$out_tar" +} + +# ───────────────────────────────────────────── +# fastly.toml updater +# ───────────────────────────────────────────── +update_fastly_toml() { + local service_name="$1" + cat > "$SCRIPT_DIR/fastly.toml" </dev/null || echo "000") + code="${code// /}" + case "$code" in + 400|404) write_ok "Relay is working. Origin responded with HTTP $code (expected for xhttp)." ;; + 200) write_ok "Relay is working. HTTP 200 OK." ;; + 000|"") write_warn "Could not reach endpoint - DNS may still be propagating. Try again in 1-2 minutes." ;; + 500) write_warn "HTTP 500 - Config Store may not be linked yet. Try redeploying in 30 seconds." ;; + 502) write_warn "HTTP 502 - Backend unreachable. Check origin server and port." ;; + *) write_warn "HTTP $code - Unexpected response. Check origin server." ;; + esac +} + +# ───────────────────────────────────────────── +# Config summary printer +# ───────────────────────────────────────────── +show_final_summary() { + local service_name="$1" service_id="$2" version="$3" domain="$4" + local target_domain="$5" relay_path="$6" + local connect_timeout="$7" first_byte="$8" between_bytes="$9" + + echo "" + echo -e "${GREEN}==============================================${NC}" + echo -e "${GREEN} Deployment Complete!${NC}" + echo -e "${GREEN}==============================================${NC}" + echo "" + write_info "Service Name : $service_name" + write_info "Service ID : $service_id" + write_info "Version : $version" + write_info "Domain : $domain" + write_info "Target : $target_domain" + write_info "Relay Path : $relay_path" + write_info "connect_timeout : ${connect_timeout}ms" + write_info "first_byte_timeout : ${first_byte}ms" + write_info "between_bytes_timeout : ${between_bytes}ms" + echo "" + echo -e "${YELLOW} Client config:${NC}" + echo "" + echo -e "${CYAN} Do you want to enter your VLESS UUID for a ready-to-use config?${NC}" + echo -e "${DARK_GRAY} (Press Enter to skip and get a sample config instead)${NC}" + local uuid_input + read -rp " VLESS UUID: " uuid_input + uuid_input="${uuid_input// /}" + uuid_input="${uuid_input//\"/}" + uuid_input="${uuid_input//\'/}" + + local encoded_path + encoded_path=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$relay_path'))" 2>/dev/null || echo "$relay_path") + local vless_base="vless://%s@${domain}:443?encryption=none&security=tls&sni=${domain}&fp=chrome&insecure=0&type=xhttp&host=${domain}&path=${encoded_path}&mode=auto#XHTTP-Fastly-Compute" + + echo "" + if [[ -n "$uuid_input" ]]; then + printf "${GREEN} Ready-to-use config:${NC}\n\n" + printf "${CYAN} ${vless_base}\n${NC}" "$uuid_input" + else + printf "${YELLOW} Sample config (replace YOUR-UUID-HERE with your VLESS UUID):${NC}\n\n" + printf "${CYAN} ${vless_base}\n${NC}" "YOUR-UUID-HERE" + fi + echo "" + echo -e "${GREEN}==============================================${NC}" + + # Save build profile + local stamp prof_path + stamp=$(date '+%Y%m%d-%H%M%S') + prof_path="$SCRIPT_DIR/build-profile-fastly-$stamp.txt" + cat > "$prof_path" </dev/null || echo 0) + if [[ "$count" -eq 0 ]]; then + write_warn "No existing Compute services found." + echo "null" + return + fi + echo "" + echo -e "${CYAN} Existing Compute services:${NC}" + echo "$services_json" | python3 - <<'PYEOF' +import sys,json +svcs=json.load(sys.stdin) +for i,s in enumerate(svcs,1): + ver = f"v{s['active_version']}" if s.get('active_version') else "no active version" + print(f" [{i}] {s['name']} ({s['id']}) [{ver}]") +PYEOF + echo " [0] Create new service" + echo "" + local pick n + pick=$(read_default "Select service" "0") + n=$(echo "$pick" | tr -dc '0-9' || echo 0) + if [[ "$n" -ge 1 && "$n" -le "$count" ]]; then + echo "$services_json" | python3 -c " +import sys,json +svcs=json.load(sys.stdin) +print(json.dumps(svcs[$((n-1))]))" 2>/dev/null || echo "null" + else + echo "null" + fi +} + +# ───────────────────────────────────────────── +# New deployment flow +# ───────────────────────────────────────────── +run_new_deploy_flow() { + local token="$1" + write_step "New deployment - collecting config..." + + local suggested_name + suggested_name="relay-$(cat /dev/urandom | tr -dc 'a-z0-9' | head -c8 2>/dev/null || echo 'xxxxxxxx')" + local service_name + service_name=$(read_default "Service name" "$suggested_name") + + echo "" + write_warn "TARGET_DOMAIN: full URL of your inbound server including port." + write_warn "Example: https://your-domain.com:2053" + local target_raw + target_raw=$(read_required "TARGET_DOMAIN") + + local parsed hostname port base valid + IFS='|' read -r hostname port base valid <<< "$(parse_target_domain "$target_raw")" + if [[ "$valid" != "true" ]]; then + write_err "Invalid TARGET_DOMAIN format. Use https://hostname:port" + return 1 + fi + + echo "" + write_warn "RELAY_PATH: the path configured on your inbound (e.g. /api)." + write_warn "PUBLIC_RELAY_PATH will be set to the same value automatically." + local relay_path_raw relay_path + relay_path_raw=$(read_default "RELAY_PATH" "/api") + relay_path=$(normalize_path "$relay_path_raw") + + echo "" + echo -e "${CYAN} Backend timeout settings:${NC}" + echo "" + write_warn " connect_timeout: Max time (ms) to establish TCP connection to your origin server." + write_warn " Default 10000ms (10s). If origin is slow to accept connections, increase this." + local connect_timeout + connect_timeout=$(read_default " connect_timeout (ms)" "10000") + + echo "" + write_warn " first_byte_timeout: Max time (ms) to wait for the FIRST byte from origin after" + write_warn " sending the request. Critical for xhttp - origin may take time to respond." + write_warn " Default 300000ms (5min). Increase if you see 503/504 errors on slow connections." + local first_byte_timeout + first_byte_timeout=$(read_default " first_byte_timeout (ms)" "300000") + + echo "" + write_warn " between_bytes_timeout: Max time (ms) to wait BETWEEN each chunk of data from origin." + write_warn " Critical for xhttp streaming - keep high to avoid mid-stream disconnects." + write_warn " Default 300000ms (5min). Lower this only if you want faster failure detection." + local between_bytes_timeout + between_bytes_timeout=$(read_default " between_bytes_timeout (ms)" "300000") + + echo "" + echo -e "${CYAN} Configuration summary:${NC}" + write_info " Service name : $service_name" + write_info " Target domain : $base" + write_info " Relay path : $relay_path" + write_info " connect_timeout : ${connect_timeout}ms" + write_info " first_byte_timeout : ${first_byte_timeout}ms" + write_info " between_bytes_timeout : ${between_bytes_timeout}ms" + echo "" + local ok + ok=$(read_yes_no "Proceed with this configuration?" "true") + if [[ "$ok" != "true" ]]; then + write_warn "Canceled. Returning to menu." + return 1 + fi + + # Export config via globals (bash doesn't have return-by-value for structs) + CFG_SERVICE_NAME="$service_name" + CFG_TARGET_DOMAIN="$base" + CFG_HOSTNAME="$hostname" + CFG_PORT="$port" + CFG_RELAY_PATH="$relay_path" + CFG_CONNECT_TIMEOUT="$connect_timeout" + CFG_FIRST_BYTE_TIMEOUT="$first_byte_timeout" + CFG_BETWEEN_BYTES_TIMEOUT="$between_bytes_timeout" + CFG_IS_NEW="true" + CFG_SERVICE_ID="" +} + +# ───────────────────────────────────────────── +# Redeploy existing service flow +# ───────────────────────────────────────────── +run_redeploy_flow() { + local token="$1" + local svc_json + svc_json=$(select_existing_service "$token") + if [[ "$svc_json" == "null" ]]; then + run_new_deploy_flow "$token" + return + fi + + local svc_name svc_id + svc_name=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null || echo "") + svc_id=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") + + write_step "Redeploying: $svc_name" + + local saved + saved=$(get_saved_state_for_service "$svc_id") + local def_target def_path + def_target=$(echo "$saved" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('TargetDomain','') if d else '')" 2>/dev/null || echo "") + def_path=$(echo "$saved" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('RelayPath','/api') if d else '/api')" 2>/dev/null || echo "/api") + + if [[ -n "$def_target" ]]; then + write_info "Last known target : $def_target" + write_info "Last known path : $def_path" + local keep + keep=$(read_yes_no "Keep these values?" "true") + if [[ "$keep" != "true" ]]; then + def_target="" + def_path="/api" + fi + fi + + local target_raw + if [[ -z "$def_target" ]]; then + target_raw=$(read_required "TARGET_DOMAIN (https://hostname:port)") + else + target_raw="$def_target" + fi + + local hostname port base valid + IFS='|' read -r hostname port base valid <<< "$(parse_target_domain "$target_raw")" + if [[ "$valid" != "true" ]]; then + write_err "Invalid TARGET_DOMAIN." + return 1 + fi + + local relay_path_raw relay_path + relay_path_raw=$(read_default "RELAY_PATH" "$def_path") + relay_path=$(normalize_path "$relay_path_raw") + + local def_connect def_first_byte def_between_bytes + def_connect=$(echo "$saved" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('ConnectTimeout',10000) if d else 10000)" 2>/dev/null || echo 10000) + def_first_byte=$(echo "$saved" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('FirstByteTimeout',300000) if d else 300000)" 2>/dev/null || echo 300000) + def_between_bytes=$(echo "$saved" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('BetweenBytesTimeout',300000) if d else 300000)" 2>/dev/null || echo 300000) + + local change_timeouts + change_timeouts=$(read_yes_no "Change backend timeout settings? (current: connect=${def_connect}ms, first_byte=${def_first_byte}ms, between=${def_between_bytes}ms)" "false") + if [[ "$change_timeouts" == "true" ]]; then + def_connect=$(read_default " connect_timeout (ms)" "$def_connect") + def_first_byte=$(read_default " first_byte_timeout (ms)" "$def_first_byte") + def_between_bytes=$(read_default " between_bytes_timeout (ms)" "$def_between_bytes") + fi + + CFG_SERVICE_NAME="$svc_name" + CFG_SERVICE_ID="$svc_id" + CFG_TARGET_DOMAIN="$base" + CFG_HOSTNAME="$hostname" + CFG_PORT="$port" + CFG_RELAY_PATH="$relay_path" + CFG_CONNECT_TIMEOUT="$def_connect" + CFG_FIRST_BYTE_TIMEOUT="$def_first_byte" + CFG_BETWEEN_BYTES_TIMEOUT="$def_between_bytes" + CFG_IS_NEW="false" +} + +# ───────────────────────────────────────────── +# Core deploy pipeline +# ───────────────────────────────────────────── +deploy_to_fastly() { + local token="$1" + + # 1. Update source files + write_step "Writing relay source..." + update_fastly_toml "$CFG_SERVICE_NAME" + write_ok "Source files updated." + + # 2. Build + ensure_js_compute_runtime + invoke_build + + # 3. Create or resolve service + local service_id="" + if [[ "$CFG_IS_NEW" == "true" ]]; then + write_step "Creating Fastly Compute service '$CFG_SERVICE_NAME'..." + local new_svc + new_svc=$(new_fastly_service "$token" "$CFG_SERVICE_NAME") + service_id=$(echo "$new_svc" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") + write_ok "Service created: $service_id" + else + service_id="$CFG_SERVICE_ID" + write_ok "Using existing service: $service_id" + fi + + # 4. Create new version + write_step "Creating new service version..." + local new_ver_json version_no + new_ver_json=$(new_fastly_service_version "$token" "$service_id") + version_no=$(echo "$new_ver_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('number',0))" 2>/dev/null || echo 0) + write_ok "Version $version_no created." + + # 5. Add domain + write_step "Adding Fastly domain..." + local domain="${CFG_SERVICE_NAME}.edgecompute.app" + local dom_result + dom_result=$(add_fastly_domain "$token" "$service_id" "$version_no" "$domain" 2>/dev/null) || { + write_warn "Could not add domain (may already exist). Using: $domain" + } + + # 5b. Push Config Store + push_config_store "$token" "$service_id" "$version_no" \ + "$CFG_SERVICE_NAME" "$CFG_TARGET_DOMAIN" "$CFG_HOSTNAME" "$CFG_RELAY_PATH" + + # 6. Add backend + write_step "Adding backend ($CFG_HOSTNAME:$CFG_PORT)..." + add_fastly_backend "$token" "$service_id" "$version_no" \ + "$CFG_HOSTNAME" "$CFG_PORT" \ + "$CFG_CONNECT_TIMEOUT" "$CFG_FIRST_BYTE_TIMEOUT" "$CFG_BETWEEN_BYTES_TIMEOUT" \ + && write_ok "Backend added." \ + || write_warn "Backend note: could not add backend." + + # 7. Package + local pkg_path + pkg_path=$(build_package "$CFG_SERVICE_NAME") + + # 8. Upload package + write_step "Uploading Wasm package to Fastly..." + upload_fastly_package "$token" "$service_id" "$version_no" "$pkg_path" \ + && write_ok "Package uploaded." \ + || { write_err "Package upload failed."; exit 1; } + + # 9. Validate + write_step "Validating version $version_no..." + local validation + validation=$(validate_fastly_version "$token" "$service_id" "$version_no") + local val_status + val_status=$(echo "$validation" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null || echo "") + if [[ "$val_status" == "ok" ]]; then + write_ok "Validation passed." + else + local errs + errs=$(echo "$validation" | python3 -c "import sys,json; d=json.load(sys.stdin); print('; '.join(d.get('errors',[])))" 2>/dev/null || echo "unknown") + write_err "Validation failed: $errs" + exit 1 + fi + + # 10. Activate + write_step "Activating version $version_no..." + activate_fastly_version "$token" "$service_id" "$version_no" > /dev/null 2>&1 + write_ok "Version $version_no is now active." + + # 11. Save state + add_deploy_state_entry "$CFG_SERVICE_NAME" "$service_id" "$domain" \ + "$CFG_TARGET_DOMAIN" "$CFG_RELAY_PATH" \ + "$CFG_CONNECT_TIMEOUT" "$CFG_FIRST_BYTE_TIMEOUT" "$CFG_BETWEEN_BYTES_TIMEOUT" + + # 12. Health check + summary + echo "" + write_warn "Waiting 25 seconds for CDN propagation..." + sleep 25 + run_health_check "$domain" "$CFG_RELAY_PATH" + show_final_summary "$CFG_SERVICE_NAME" "$service_id" "$version_no" "$domain" \ + "$CFG_TARGET_DOMAIN" "$CFG_RELAY_PATH" \ + "$CFG_CONNECT_TIMEOUT" "$CFG_FIRST_BYTE_TIMEOUT" "$CFG_BETWEEN_BYTES_TIMEOUT" +} + +# ───────────────────────────────────────────── +# Auth flow +# ───────────────────────────────────────────── +ensure_fastly_token() { + write_step "Fastly API token..." + local saved + saved=$(load_token_secure) + if [[ -n "$saved" ]]; then + write_info "Saved encrypted token found." + local use + use=$(read_yes_no "Use saved token?" "true") + if [[ "$use" == "true" ]]; then + if test_fastly_token "$saved"; then + write_ok "Token validated." + echo "$saved" + return + fi + write_warn "Saved token is invalid. Please enter a new one." + fi + fi + + echo "" + write_warn "Create a token at: https://manage.fastly.com/account/personal/tokens" + write_warn "Required scope: global (or at minimum: purge_all, engineer)" + echo "" + local token + token=$(read_required "Paste your Fastly API token") + token="${token// /}" + token="${token//\"/}" + token="${token//\'/}" + + write_step "Validating token..." + if ! test_fastly_token "$token"; then + write_err "Token validation failed. Check the token and try again." + exit 1 + fi + write_ok "Token is valid." + + local save + save=$(read_yes_no "Save token encrypted on this machine?" "true") + if [[ "$save" == "true" ]]; then + save_token_secure "$token" + write_ok "Token saved: $TOKEN_STORE" + fi + + echo "$token" +} + +# ───────────────────────────────────────────── +# Manage Services helpers +# ───────────────────────────────────────────── +get_service_config_store_summary() { + local token="$1" service_id="$2" version="$3" + local resources + resources=$(fastly_api GET "/service/$service_id/version/$version/resource" "$token" 2>/dev/null) || { echo "null"; return; } + python3 - "$token" "$service_id" "$version" </dev/null) || { echo "{}"; return; } + echo "$result" | python3 -c " +import sys,json +items=json.load(sys.stdin) +if not isinstance(items, list): items=[] +d={item.get('item_key',''): item.get('item_value','') for item in items} +print(json.dumps(d)) +" 2>/dev/null || echo "{}" +} + +show_service_config_summary() { + local token="$1" svc_json="$2" + local service_id service_name active_version + service_id=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") + service_name=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null || echo "") + active_version=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('active_version',0) or 0)" 2>/dev/null || echo 0) + + if [[ "$active_version" -eq 0 ]]; then write_warn "No active version found."; return; fi + + echo "" + echo -e "${CYAN} ============================================${NC}" + echo -e " Service: $service_name" + echo -e "${CYAN} ============================================${NC}" + write_info " Service ID : $service_id" + write_info " Domain : $service_name.edgecompute.app" + write_info " Version : $active_version" + + local cfg_summary + cfg_summary=$(get_service_config_store_summary "$token" "$service_id" "$active_version") + if [[ "$cfg_summary" != "null" ]]; then + local store_id + store_id=$(echo "$cfg_summary" | python3 -c "import sys,json; print(json.load(sys.stdin).get('store_id',''))" 2>/dev/null || echo "") + if [[ -n "$store_id" ]]; then + local items + items=$(get_service_config_store_items "$token" "$store_id") + echo "" + echo -e "${YELLOW} Config Store (ENV):${NC}" + local target_base target_host relay_path + target_base=$(echo "$items" | python3 -c "import sys,json; print(json.load(sys.stdin).get('TARGET_BASE',''))" 2>/dev/null || echo "") + target_host=$(echo "$items" | python3 -c "import sys,json; print(json.load(sys.stdin).get('TARGET_HOSTNAME',''))" 2>/dev/null || echo "") + relay_path=$(echo "$items" | python3 -c "import sys,json; print(json.load(sys.stdin).get('RELAY_PATH',''))" 2>/dev/null || echo "") + write_info " TARGET_BASE : $target_base" + write_info " TARGET_HOSTNAME : $target_host" + write_info " RELAY_PATH : $relay_path" + fi + else + write_warn " No Config Store linked." + fi + + local backend + backend=$(fastly_api GET "/service/$service_id/version/$active_version/backend" "$token" 2>/dev/null) || backend="[]" + local first_backend + first_backend=$(echo "$backend" | python3 -c " +import sys,json +b=json.load(sys.stdin) +if isinstance(b,list) and b: print(json.dumps(b[0])) +else: print('null') +" 2>/dev/null || echo "null") + if [[ "$first_backend" != "null" ]]; then + echo "" + echo -e "${YELLOW} Backend timeouts:${NC}" + local ct fbt bbt + ct=$(echo "$first_backend" | python3 -c "import sys,json; print(json.load(sys.stdin).get('connect_timeout',''))" 2>/dev/null || echo "") + fbt=$(echo "$first_backend" | python3 -c "import sys,json; print(json.load(sys.stdin).get('first_byte_timeout',''))" 2>/dev/null || echo "") + bbt=$(echo "$first_backend" | python3 -c "import sys,json; print(json.load(sys.stdin).get('between_bytes_timeout',''))" 2>/dev/null || echo "") + write_info " connect_timeout : ${ct}ms" + write_info " first_byte_timeout : ${fbt}ms" + write_info " between_bytes_timeout : ${bbt}ms" + fi + echo -e "${CYAN} ============================================${NC}" +} + +edit_service_config_store() { + local token="$1" svc_json="$2" + local service_id service_name active_version + service_id=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") + service_name=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null || echo "") + active_version=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('active_version',0) or 0)" 2>/dev/null || echo 0) + if [[ "$active_version" -eq 0 ]]; then write_warn "No active version found."; return; fi + + local cfg_summary + cfg_summary=$(get_service_config_store_summary "$token" "$service_id" "$active_version") + if [[ "$cfg_summary" == "null" ]]; then + write_warn "No Config Store linked to this service." + return + fi + + local store_id + store_id=$(echo "$cfg_summary" | python3 -c "import sys,json; print(json.load(sys.stdin).get('store_id',''))" 2>/dev/null || echo "") + local items + items=$(get_service_config_store_items "$token" "$store_id") + + echo "" + echo -e "${CYAN} Edit Config Store for: $service_name${NC}" + echo -e "${DARK_GRAY} (Press Enter to keep current value)${NC}" + echo "" + + local cur_target_base cur_target_hostname cur_relay_path + cur_target_base=$(echo "$items" | python3 -c "import sys,json; print(json.load(sys.stdin).get('TARGET_BASE',''))" 2>/dev/null || echo "") + cur_target_hostname=$(echo "$items" | python3 -c "import sys,json; print(json.load(sys.stdin).get('TARGET_HOSTNAME',''))" 2>/dev/null || echo "") + cur_relay_path=$(echo "$items" | python3 -c "import sys,json; print(json.load(sys.stdin).get('RELAY_PATH',''))" 2>/dev/null || echo "") + + local new_val changed=false + + write_info " Current TARGET_BASE: $cur_target_base" + read -rp " New value for TARGET_BASE (e.g. https://domain.com:2053): " new_val + new_val="${new_val// /}" + if [[ -n "$new_val" && "$new_val" != "$cur_target_base" ]]; then + set_fastly_config_store_item "$token" "$store_id" "TARGET_BASE" "$new_val" && write_ok "Updated: TARGET_BASE" || write_warn "Failed to update TARGET_BASE" + changed=true + fi + + write_info " Current TARGET_HOSTNAME: $cur_target_hostname" + read -rp " New value for TARGET_HOSTNAME (e.g. domain.com): " new_val + new_val="${new_val// /}" + if [[ -n "$new_val" && "$new_val" != "$cur_target_hostname" ]]; then + set_fastly_config_store_item "$token" "$store_id" "TARGET_HOSTNAME" "$new_val" && write_ok "Updated: TARGET_HOSTNAME" || write_warn "Failed to update TARGET_HOSTNAME" + changed=true + fi + + write_info " Current RELAY_PATH: $cur_relay_path" + read -rp " New value for RELAY_PATH (e.g. /api): " new_val + new_val="${new_val// /}" + if [[ -n "$new_val" && "$new_val" != "$cur_relay_path" ]]; then + set_fastly_config_store_item "$token" "$store_id" "RELAY_PATH" "$new_val" && write_ok "Updated: RELAY_PATH" || write_warn "Failed to update RELAY_PATH" + changed=true + fi + + if [[ "$changed" == "false" ]]; then + write_warn "No changes made." + else + write_ok "Config Store updated. Changes are live immediately - no redeploy needed." + fi +} + +remove_fastly_service() { + local token="$1" svc_json="$2" + local service_id service_name active_version + service_id=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") + service_name=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null || echo "") + active_version=$(echo "$svc_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('active_version',0) or 0)" 2>/dev/null || echo 0) + + echo "" + echo -e "${RED} Service to delete: $service_name${NC}" + echo -e "${RED} Service ID : $service_id${NC}" + echo "" + local confirm + confirm=$(read_yes_no "Are you sure you want to DELETE this service? This cannot be undone." "false") + if [[ "$confirm" != "true" ]]; then write_warn "Canceled."; return; fi + + if [[ "$active_version" -gt 0 ]]; then + fastly_api PUT "/service/$service_id/version/$active_version/deactivate" "$token" > /dev/null 2>&1 \ + && write_ok "Version $active_version deactivated." \ + || write_warn "Could not deactivate version." + fi + + fastly_api DELETE "/service/$service_id" "$token" > /dev/null 2>&1 \ + && write_ok "Service '$service_name' deleted." \ + || { write_err "Delete failed."; return; } + + local existing updated + existing=$(load_deploy_state) + updated=$(echo "$existing" | python3 -c " +import sys,json +svcs=json.load(sys.stdin) +print(json.dumps([s for s in svcs if s.get('ServiceId')!='$service_id'])) +" 2>/dev/null || echo "[]") + save_deploy_state "$updated" +} + +select_service_from_fastly() { + local token="$1" + write_step "Fetching Compute services from Fastly..." + local services_json + services_json=$(get_fastly_services "$token") + local count + count=$(echo "$services_json" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) + if [[ "$count" -eq 0 ]]; then + write_warn "No Compute services found on this account." + echo "null" + return + fi + echo "" + echo -e "${CYAN} Your Fastly Compute services:${NC}" + echo "" + echo "$services_json" | python3 - <<'PYEOF' +import sys,json +svcs=json.load(sys.stdin) +for i,s in enumerate(svcs,1): + ver = f"v{s['active_version']}" if s.get('active_version') else "no active version" + print(f" [{i}] {s['name']}") + print(f" ID: {s['id']} | {ver}") +PYEOF + echo "" + echo " [0] Back" + echo "" + local pick n + pick=$(read_default "Select service" "0") + n=$(echo "$pick" | tr -dc '0-9' || echo 0) + if [[ "$n" -ge 1 && "$n" -le "$count" ]]; then + echo "$services_json" | python3 -c " +import sys,json +svcs=json.load(sys.stdin) +print(json.dumps(svcs[$((n-1))]))" 2>/dev/null || echo "null" + else + echo "null" + fi +} + +show_manage_services_menu() { + local token="$1" + while true; do + write_banner + echo -e "${CYAN} Manage Services${NC}" + echo "" + echo -e "${GREEN} [1] List all services - show all Compute services on your account${NC}" + echo -e "${GREEN} [2] View service details - config, ENV, timeouts for a service${NC}" + echo -e "${YELLOW} [3] Edit ENV (Config Store) - change TARGET, PATH without redeploy${NC}" + echo -e "${RED} [4] Delete service - permanently remove a service${NC}" + echo " [0] Back to main menu" + echo "" + + local pick + pick=$(read_default "Select" "0") + case "${pick// /}" in + 1) + write_step "All Compute services on your account:" + local services_json + services_json=$(get_fastly_services "$token") + local count + count=$(echo "$services_json" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) + if [[ "$count" -eq 0 ]]; then + write_warn "No Compute services found." + else + echo "" + echo "$services_json" | python3 - <<'PYEOF' +import sys,json +svcs=json.load(sys.stdin) +for s in svcs: + ver = f"v{s['active_version']}" if s.get('active_version') else "no active version" + print(f" {s['name']}") + print(f" ID: {s['id']} | {ver} | {s['name']}.edgecompute.app") + print() +PYEOF + fi + read -rp "Press Enter to continue" _ + ;; + 2) + local svc_json + svc_json=$(select_service_from_fastly "$token") + if [[ "$svc_json" != "null" ]]; then + show_service_config_summary "$token" "$svc_json" + read -rp $'\nPress Enter to continue' _ + fi + ;; + 3) + local svc_json + svc_json=$(select_service_from_fastly "$token") + if [[ "$svc_json" != "null" ]]; then + edit_service_config_store "$token" "$svc_json" + read -rp $'\nPress Enter to continue' _ + fi + ;; + 4) + local svc_json + svc_json=$(select_service_from_fastly "$token") + if [[ "$svc_json" != "null" ]]; then + remove_fastly_service "$token" "$svc_json" + read -rp $'\nPress Enter to continue' _ + fi + ;; + 0) return ;; + *) write_warn "Invalid selection." ;; + esac + done +} + +# ───────────────────────────────────────────── +# Main menu +# ───────────────────────────────────────────── +show_main_menu() { + local token="$1" + while true; do + write_banner + echo -e "${CYAN} Main Menu${NC}" + echo "" + echo -e "${GREEN} [1] New deployment - create a new Fastly Compute service${NC}" + echo -e "${GREEN} [2] Redeploy / update - push new build to an existing service${NC}" + echo -e "${CYAN} [3] Manage services - list, view, edit ENV, delete services${NC}" + echo -e "${DARK_YELLOW} [4] Change API token - replace the saved Fastly token${NC}" + echo " [0] Exit" + echo "" + + local pick + pick=$(read_default "Select" "1") + case "${pick// /}" in + 1) + if run_new_deploy_flow "$token"; then + deploy_to_fastly "$token" + read -rp $'\nPress Enter to return to menu' _ + fi + ;; + 2) + if run_redeploy_flow "$token"; then + deploy_to_fastly "$token" + read -rp $'\nPress Enter to return to menu' _ + fi + ;; + 3) + show_manage_services_menu "$token" + ;; + 4) + rm -f "$TOKEN_STORE" + token=$(ensure_fastly_token) + read -rp $'\nToken updated. Press Enter to continue' _ + ;; + 0) + echo -e "${CYAN}Goodbye!${NC}" + exit 0 + ;; + *) + write_warn "Invalid selection." + ;; + esac + done +} + +# ───────────────────────────────────────────── +# Entry point +# ───────────────────────────────────────────── +write_banner +echo -e "${RED} Important: connect VPN (TUN mode) before continuing.${NC}" +echo -e "${DARK_YELLOW} Tip: Ctrl+C to exit at any time.${NC}" +echo "" + +if ! command -v python3 &>/dev/null; then + echo -e "${RED}Error: python3 is required but not found. Please install python3.${NC}" + exit 1 +fi +if ! command -v curl &>/dev/null; then + echo -e "${RED}Error: curl is required but not found. Please install curl.${NC}" + exit 1 +fi +if ! command -v openssl &>/dev/null; then + echo -e "${RED}Error: openssl is required but not found. Please install openssl.${NC}" + exit 1 +fi + +ensure_node +FASTLY_TOKEN=$(ensure_fastly_token) +show_main_menu "$FASTLY_TOKEN" From f4bf89376c7a223ac48ab7422914f05f9be8c8c1 Mon Sep 17 00:00:00 2001 From: alirahimi818 Date: Fri, 5 Jun 2026 21:31:04 +0200 Subject: [PATCH 2/3] Update README: add Linux/macOS installation guide --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4a08994..f1e8b60 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Runtime](https://img.shields.io/badge/Runtime-Fastly_Compute_JS-FF282D.svg?style=for-the-badge&logo=fastly)]() [![Installer](https://img.shields.io/badge/Windows_Installer-Token_API_Mode-blue.svg?style=for-the-badge)]() +[![Linux](https://img.shields.io/badge/Linux%2FmacOS_Installer-Bash_Script-orange.svg?style=for-the-badge&logo=linux)]() [![Config](https://img.shields.io/badge/Config-Config_Store_(ENV)-2ea44f.svg?style=for-the-badge)]() **داستان این نسخه چیه؟** @@ -15,7 +16,9 @@ 📣 **جهت دریافت اطلاعات و نکات بیشتر به کانال تلگرامی من مراجعه کنید:** [@B3hnamR](https://t.me/B3hnamR) 📌 **نکته مهم:** لطفاً این راهنما رو تا انتها و با دقت بخونید تا موقع ستاپ کردن هیچ مشکلی براتون پیش نیاد. -> ⚠️ **هشدار:** پروژه رو **Fork نکنید**. برای امنیت بیشتر از دکمه سبز **Code** بالای صفحه روی **Download ZIP** کلیک کنید و از اینستالر ویندوزی استفاده کنید. +> ⚠️ **هشدار:** برای امنیت بیشتر از دکمه سبز **Code** بالای صفحه روی **Download ZIP** کلیک کنید. +> 🪟 **ویندوز:** از `Run-Deploy-Fastly.bat` استفاده کنید. +> 🐧 **لینوکس / مک:** از `Deploy-Fastly-Linux.sh` استفاده کنید. @@ -69,6 +72,42 @@ ۴. توکن Fastly رو وارد کن ۵. تنظیمات رو پر کن و Deploy بزن +توکن به صورت **رمزنگاری‌شده با DPAPI ویندوز** ذخیره میشه: + +```text +.fastly-token.dpapi +``` + +--- + +## 🐧 نصب خودکار روی لینوکس / مک + +**پیش‌نیازها:** `bash`، `curl`، `python3`، `openssl`، `npm` + +روی اکثر توزیع‌های لینوکس (Ubuntu، Debian، Arch و ...) و macOS این ابزارها از پیش نصب هستن یا به راحتی نصب میشن. + +**مراحل:** + +۱. فایل ZIP پروژه رو دانلود و Extract کن +۲. فیلترشکن رو روشن کن (TUN Mode) +۳. به پوشه پروژه برو و اسکریپت رو اجرا کن: + +```bash +chmod +x Deploy-Fastly-Linux.sh +./Deploy-Fastly-Linux.sh +``` + +۴. توکن Fastly رو وارد کن +۵. تنظیمات رو پر کن و Deploy بزن + +> **نکته:** اگه `npm` نصب نیست، اسکریپت سعی می‌کنه Node.js رو خودکار نصب کنه (از طریق `apt`، `brew` یا `dnf`). + +توکن به صورت **رمزنگاری‌شده با AES-256** ذخیره میشه: + +```text +.fastly-token.enc +``` + ### ساخت توکن Fastly ۱. وارد [manage.fastly.com](https://manage.fastly.com) بشو @@ -76,12 +115,6 @@ ۳. یک توکن جدید با scope **global** بساز ۴. توکن رو داخل اینستالر پیست کن -توکن به صورت **رمزنگاری‌شده با DPAPI ویندوز** ذخیره میشه: - -```text -.fastly-token.dpapi -``` - --- ## 🎛️ منوی اینستالر @@ -225,8 +258,9 @@ vless://YOUR-UUID@relay-xxxxxxxx.edgecompute.app:443 ```text XHTTPRelayFastly/ -├── Deploy-Fastly-Windows.ps1 ← اینستالر ویندوزی -├── Run-Deploy-Fastly.bat ← لانچر یک‌کلیکی +├── Deploy-Fastly-Windows.ps1 ← اینستالر ویندوزی (PowerShell) +├── Deploy-Fastly-Linux.sh ← اینستالر لینوکس / مک (Bash) +├── Run-Deploy-Fastly.bat ← لانچر یک‌کلیکی ویندوز ├── fastly.toml ← تنظیمات Fastly Compute ├── package.json ← وابستگی‌های JS ├── src/ From 71c5c8ac7917296c58ba60ae0238b057b18b2617 Mon Sep 17 00:00:00 2001 From: alirahimi818 Date: Fri, 5 Jun 2026 21:49:08 +0200 Subject: [PATCH 3/3] Fix curl error handling and Node.js install security - P1: add HTTP status checking to add_fastly_backend and upload_fastly_package instead of silently discarding curl output - P2: download NodeSource setup script to temp file before sudo execution instead of piping directly to sudo bash --- Deploy-Fastly-Linux.sh | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Deploy-Fastly-Linux.sh b/Deploy-Fastly-Linux.sh index fbf12f6..ef3dbf3 100755 --- a/Deploy-Fastly-Linux.sh +++ b/Deploy-Fastly-Linux.sh @@ -250,12 +250,17 @@ add_fastly_backend() { local form="name=origin_xhttp&address=$hostname&port=$port&use_ssl=1&ssl_check_cert=1" form+="&ssl_sni_hostname=$hostname&ssl_cert_hostname=$hostname&override_host=$hostname" form+="&connect_timeout=$connect_timeout&first_byte_timeout=$first_byte&between_bytes_timeout=$between_bytes" - curl -s -X POST "$uri" \ + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$uri" \ -H "Fastly-Key: $token" \ -H "Accept: application/json" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data "$form" \ - --max-time 30 > /dev/null 2>&1 + --max-time 30) + if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then + write_warn "Backend API returned HTTP $http_code." + return 1 + fi } validate_fastly_version() { @@ -271,11 +276,16 @@ activate_fastly_version() { upload_fastly_package() { local token="$1" service_id="$2" version="$3" pkg_path="$4" local uri="https://api.fastly.com/service/$service_id/version/$version/package" - curl -s -X PUT "$uri" \ + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$uri" \ -H "Fastly-Key: $token" \ -H "Accept: application/json" \ -F "package=@$pkg_path" \ - --max-time 300 > /dev/null 2>&1 + --max-time 300) + if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then + write_err "Package upload failed: HTTP $http_code." + return 1 + fi } get_fastly_active_version() { @@ -393,7 +403,11 @@ ensure_node() { write_step "npm not found. Attempting to install Node.js..." if command -v apt-get &>/dev/null; then write_info "Detected apt. Installing Node.js LTS via NodeSource..." - curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - 2>/dev/null + local setup_script + setup_script=$(mktemp) + curl -fsSL -o "$setup_script" https://deb.nodesource.com/setup_lts.x + sudo bash "$setup_script" + rm -f "$setup_script" sudo apt-get install -y nodejs 2>/dev/null elif command -v brew &>/dev/null; then write_info "Detected Homebrew. Installing Node.js LTS..."