#!/usr/bin/env bash set -euo pipefail # Usage: ./gen-client-from-url.sh "vless://uuid@host:443?type=tcp&security=reality&pbk=PUBLIC_KEY&fp=random&sni=yahoo.com&sid=SHORTID&spx=%2F&flow=xtls-rprx-vision#tag" [output.json] # If output not set, defaults to client.json URL_INPUT=${1:-} OUT_FILE=${2:-client.json} TEMPLATE_DIR="$(cd "$(dirname "$0")" && pwd)" TEMPLATE_FILE="$TEMPLATE_DIR/client.template.json" if [[ -z "$URL_INPUT" ]]; then echo "Error: provide VLESS reality URL or Subscription URL" >&2 exit 1 fi if [[ ! -f "$TEMPLATE_FILE" ]]; then echo "Template not found: $TEMPLATE_FILE" >&2 exit 1 fi # Detect if input is a subscription link (HTTP/HTTPS) if [[ "$URL_INPUT" =~ ^http ]]; then echo "Detecting subscription link..." # Build URL with client parameter for APIs that require it SUB_URL="$URL_INPUT" # Try fetching as-is first SUB_CONTENT=$(curl -sSL "$SUB_URL") # If empty, try adding client parameter (some APIs require this) if [[ -z "$SUB_CONTENT" ]]; then echo "Empty response, trying with client=v2rayng parameter..." if [[ "$SUB_URL" == *"?"* ]]; then SUB_URL="${URL_INPUT}&client=v2rayng" else SUB_URL="${URL_INPUT}?client=v2rayng" fi SUB_CONTENT=$(curl -sSL "$SUB_URL") fi if [[ -z "$SUB_CONTENT" ]]; then echo "Error: Failed to download subscription from $SUB_URL" >&2 exit 1 fi # Check if base64 encoded (simple check: no spaces, looks like b64) # Trying to decode. If fails, assume it's plain text lists if DECODED=$(echo "$SUB_CONTENT" | base64 -d 2>/dev/null); then echo "Decoded base64 subscription." RAW_CONFIGS="$DECODED" else echo "Using plain text subscription." RAW_CONFIGS="$SUB_CONTENT" fi # Find first vless reality link (vless://... + security=reality or just vless://) # We try to find one that explicitly has reality, if not, pick ANY vless TARGET_URL=$(echo "$RAW_CONFIGS" | grep -o 'vless://[^[:space:]]*' | grep 'security=reality' | head -n 1) if [[ -z "$TARGET_URL" ]]; then echo "No VLESS Reality link found, trying any VLESS..." TARGET_URL=$(echo "$RAW_CONFIGS" | grep -o 'vless://[^[:space:]]*' | head -n 1) fi if [[ -z "$TARGET_URL" ]]; then echo "Error: No VLESS URL found in subscription." >&2 exit 1 fi echo "Selected URL from subscription: ${TARGET_URL:0:30}..." URL_INPUT="$TARGET_URL" fi # Strip scheme URL_NOSCHEME=${URL_INPUT#vless://} UUID_HOST_PORT=${URL_NOSCHEME%%\?*} QUERY_AND_TAG=${URL_NOSCHEME#*?} QUERY=${QUERY_AND_TAG%%#*} TAG_RAW=${URL_INPUT#*#} TAG=${TAG_RAW:-reality} UUID=${UUID_HOST_PORT%%@*} HOST_PORT=${UUID_HOST_PORT#*@} HOST=${HOST_PORT%%:*} PORT=${HOST_PORT##*:} # Parse query params (portable, no associative arrays) PBK=""; FINGERPRINT="chrome"; SNI=""; SHORT_ID=""; SPX=""; FLOW="" OLD_IFS=$IFS IFS='&' set +u for kv in $QUERY; do key=${kv%%=*} val=${kv#*=} case "$key" in pbk) PBK=$val ;; fp) FINGERPRINT=$val ;; sni) SNI=$val ;; sid) SHORT_ID=$val ;; spx) SPX=$val ;; flow) FLOW=$val ;; esac done set -u IFS=$OLD_IFS SNI=${SNI:-$HOST} # SPX currently not used if [[ -z "$UUID" || -z "$HOST" || -z "$PORT" || -z "$PBK" || -z "$SHORT_ID" ]]; then echo "Missing required fields (uuid/host/port/pbk/sid)" >&2 exit 1 fi TMP=$(mktemp) cp "$TEMPLATE_FILE" "$TMP" # Perform replacements safely using jq # Replace simple placeholders jq \ --arg uuid "$UUID" \ --arg server "$HOST" \ --argjson port "$PORT" \ --arg tag "$TAG" \ --arg sni "$SNI" \ --arg fp "$FINGERPRINT" \ --arg pk "$PBK" \ --arg sid "$SHORT_ID" \ --arg flow "$FLOW" ' (.outbounds[] | select(.type=="vless")) as $v | ( .outbounds |= map(if .type=="vless" then ( .uuid=$uuid | .server=$server | .server_port=$port | .tag=$tag | .tls.server_name=$sni | .tls.utls.fingerprint=$fp | .tls.reality.public_key=$pk | .tls.reality.short_id=$sid | .flow=$flow ) else . end) | .route.final=$tag )' "$TMP" > "$OUT_FILE" rm "$TMP" echo "Generated $OUT_FILE from URL (tag=$TAG)"