Last active
          October 26, 2025 08:36 
        
      - 
      
 - 
        
Save NF1198/d0ffc5b510eb7a33651123771396f816 to your computer and use it in GitHub Desktop.  
    Tests fast.com from bash
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | #!/usr/bin/env bash | |
| # fast-curl.sh — FAST.com-style download test using curl | |
| # | |
| # Behavior (STDOUT vs STDERR): | |
| # • Default (silent): step-named progress to STDERR; final summary line to STDOUT: | |
| # "(<city>, <CC>) <hostname> — <XX.XX> Mbps" | |
| # • --verbose: detailed diagnostics to STDERR (warmup details, per-stream speeds, etc.); | |
| # final summary line to STDOUT (same as silent). | |
| # • --smokeping: step-named progress to STDERR; STDOUT prints one numeric Mbps value per stream | |
| # (no units/labels), suitable for SmokePing multi-sample ingestion. | |
| # • --smokeping --verbose: diagnostics to STDERR; per-stream numeric lines to STDOUT. | |
| # | |
| # Options: | |
| # --interface IFACE Network interface (default: eth0) | |
| # --streams N Number of parallel streams (default: 5) | |
| # --duration SEC Warm-up duration per candidate URL (default: 3) | |
| # --verbose Enable detailed diagnostics (to STDERR) | |
| # --smokeping Change only the final output format (see above) | |
| # --help Show usage | |
| # | |
| # Requirements: bash, curl, awk, sed, grep, sort, xargs | |
| # Optional : jq, bc (script falls back to POSIX tools if missing) | |
| set -euo pipefail | |
| # --------------------------- Defaults --------------------------- | |
| INTERFACE="eth0" | |
| STREAMS=5 | |
| DURATION=3 | |
| VERBOSE=false | |
| SMOKEPING=false | |
| # Token reverse-engineered from FAST.com bundle (works with /netflix/speedtest/v2) | |
| TOKEN="YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm" | |
| API_BASE="https://api.fast.com/netflix/speedtest/v2" | |
| TMPDIR="$(mktemp -d)" | |
| cleanup() { rm -rf "$TMPDIR"; } | |
| trap cleanup EXIT | |
| # --------------------------- Helpers ---------------------------- | |
| have() { command -v "$1" >/dev/null 2>&1; } | |
| # Step-named progress (goes to STDERR). Shown in all modes except if explicitly suppressed (not needed here). | |
| step() { | |
| # In smokeping mode we KEEP step messages, as requested. | |
| printf '%s\n' "$*" >&2 | |
| } | |
| # Detailed logs to STDERR only when --verbose is set. | |
| detail() { | |
| $VERBOSE || return 0 | |
| printf '%s\n' "$*" >&2 | |
| } | |
| die() { | |
| printf 'Error: %s\n' "$*" >&2 | |
| exit 1 | |
| } | |
| # Floating math: prefer bc; else awk | |
| calc() { | |
| if have bc; then | |
| echo "$1" | bc -l | |
| else | |
| awk "BEGIN { printf(\"%.12f\", $1) }" | |
| fi | |
| } | |
| two_dec() { | |
| # format to two decimals | |
| awk -v n="$1" 'BEGIN { printf("%.2f", n) }' | |
| } | |
| url_to_host() { | |
| # strip scheme and path/query -> hostname | |
| local url="$1" | |
| url="${url#http://}"; url="${url#https://}" | |
| printf '%s\n' "$url" | awk -F/ '{print $1}' | |
| } | |
| usage() { | |
| cat <<EOF | |
| Usage: $0 [options] | |
| Options: | |
| -i, --interface <iface> Network interface to use (default: eth0) | |
| -s, --streams <num> Number of parallel streams (default: 5) | |
| -d, --duration <sec> Warm-up duration per URL (default: 3) | |
| --verbose Verbose diagnostics to STDERR | |
| --smokeping Print one numeric Mbps per stream to STDOUT (no units/labels) | |
| -h, --help Show this help and exit | |
| Examples: | |
| $0 | |
| $0 --interface wlan0 --streams 8 --duration 5 | |
| $0 --smokeping | |
| $0 --smokeping --verbose | |
| EOF | |
| } | |
| # --------------------------- Parse CLI -------------------------- | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -i|--interface) INTERFACE="${2:-}"; shift 2 ;; | |
| -s|--streams) STREAMS="${2:-}"; shift 2 ;; | |
| -d|--duration) DURATION="${2:-}"; shift 2 ;; | |
| --verbose) VERBOSE=true; shift ;; | |
| --smokeping) SMOKEPING=true; shift ;; | |
| -h|--help) usage; exit 0 ;; | |
| --*) shift ;; # Ignore unknown long options | |
| *) HOST="$1"; shift ;; # Accept positional host (e.g., 'dummy') | |
| esac | |
| done | |
| # --------------------------- Sanity checks ---------------------- | |
| [[ -n "$INTERFACE" ]] || die "Interface is empty" | |
| [[ "$STREAMS" =~ ^[0-9]+$ ]] || die "--streams must be an integer" | |
| [[ "$DURATION" =~ ^[0-9]+$ ]] || die "--duration must be an integer" | |
| have curl || die "curl is required" | |
| have awk || die "awk is required" | |
| have sed || die "sed is required" | |
| have grep || die "grep is required" | |
| have sort || die "sort is required" | |
| have xargs || die "xargs is required" | |
| # --------------------------- Fetch targets ---------------------- | |
| API_URL="${API_BASE}?https=true&token=${TOKEN}&urlCount=${STREAMS}" | |
| step "📡 Fetching ${STREAMS} candidate targets…" | |
| RAW_JSON="$(curl -sS "$API_URL" || true)" | |
| [[ -n "$RAW_JSON" ]] || die "Failed to retrieve target JSON" | |
| TARGETS_FILE="$TMPDIR/targets.tsv" | |
| > "$TARGETS_FILE" | |
| if have jq; then | |
| # url, city, country (tab-separated -> piped to | delimited) | |
| echo "$RAW_JSON" | jq -r '.targets[] | [ .url, (.location.city // ""), (.location.country // "") ] | @tsv' \ | |
| | awk -F'\t' '{printf "%s|%s|%s\n", $1, $2, $3}' > "$TARGETS_FILE" | |
| else | |
| # Fallback parser for current API shape | |
| echo "$RAW_JSON" \ | |
| | tr -d '\n' \ | |
| | sed -n 's/.*"targets":\[\(.*\)\].*/\1/p' \ | |
| | sed 's/},{"name"/}\n{"name"/g' \ | |
| | while IFS= read -r obj; do | |
| url=$(echo "$obj" | sed -n 's/.*"url":"\([^"]*\)".*/\1/p') | |
| city=$(echo "$obj" | sed -n 's/.*"city":"\([^"]*\)".*/\1/p') | |
| country=$(echo "$obj" | sed -n 's/.*"country":"\([^"]*\)".*/\1/p') | |
| [[ -n "$url" ]] && printf '%s|%s|%s\n' "$url" "$city" "$country" | |
| done > "$TARGETS_FILE" | |
| fi | |
| [[ -s "$TARGETS_FILE" ]] || die "No targets parsed from API response" | |
| # --------------------------- Warm-up each ----------------------- | |
| step "⚙️ Benchmarking each candidate for ${DURATION}s on interface ${INTERFACE}…" | |
| WARMUP_FILE="$TMPDIR/warmup.tsv" | |
| > "$WARMUP_FILE" | |
| idx=1 | |
| while IFS='|' read -r URL CITY COUNTRY; do | |
| detail " [${idx}/${STREAMS}] probing…" | |
| SPEED_BPS="$(curl --interface "$INTERFACE" -L "$URL" \ | |
| --max-time "$DURATION" -o /dev/null \ | |
| -w '%{speed_download}' 2>/dev/null || echo 0)" | |
| printf "%s\t%s\t%s\t%s\n" "$SPEED_BPS" "$URL" "$CITY" "$COUNTRY" >> "$WARMUP_FILE" | |
| if $VERBOSE; then | |
| mbps="$(calc "$SPEED_BPS*8/1000000")" | |
| detail " -> $(two_dec "$mbps") Mbps" | |
| fi | |
| idx=$((idx+1)) | |
| done < "$TARGETS_FILE" | |
| FASTEST_LINE="$(sort -nr -k1,1 "$WARMUP_FILE" | head -n1 || true)" | |
| [[ -n "$FASTEST_LINE" ]] || die "Warm-up produced no results" | |
| FASTEST_BPS="$(echo "$FASTEST_LINE" | awk -F'\t' '{print $1}')" | |
| FASTEST_URL="$(echo "$FASTEST_LINE" | awk -F'\t' '{print $2}')" | |
| FASTEST_CITY="$(echo "$FASTEST_LINE" | awk -F'\t' '{print $3}')" | |
| FASTEST_CC="$(echo "$FASTEST_LINE" | awk -F'\t' '{print $4}')" | |
| FAST_HOST="$(url_to_host "$FASTEST_URL")" | |
| FAST_MBPS="$(calc "$FASTEST_BPS*8/1000000")" | |
| FAST_MBPS_FMT="$(two_dec "$FAST_MBPS")" | |
| step "🏁 Fastest candidate selected." | |
| detail " Host: $FAST_HOST" | |
| detail " Loc : ${FASTEST_CITY:-?}, ${FASTEST_CC:-?}" | |
| detail " Warm: ${FAST_MBPS_FMT} Mbps" | |
| # --------------------------- Parallel test --------------------- | |
| step "🚀 Running parallel test: ${STREAMS} streams on ${INTERFACE}" | |
| SPEED_DIR="$TMPDIR/streams"; mkdir -p "$SPEED_DIR" | |
| for i in $(seq 1 "$STREAMS"); do | |
| ( | |
| curl --interface "$INTERFACE" -L "$FASTEST_URL" \ | |
| -o /dev/null -w '%{speed_download}' 2>/dev/null \ | |
| > "$SPEED_DIR/s_$i.txt" | |
| ) & | |
| done | |
| wait | |
| # Gather per-stream speeds (bytes/sec) and compute Mbps per stream. | |
| PER_STREAM_MBPS=() | |
| TOTAL_BPS="0" | |
| for f in "$SPEED_DIR"/s_*.txt; do | |
| SPD_BPS="$(cat "$f" 2>/dev/null || echo 0)" | |
| TOTAL_BPS="$(calc "$TOTAL_BPS+$SPD_BPS")" | |
| MBPS="$(calc "$SPD_BPS*8/1000000")" | |
| MBPS_FMT="$(two_dec "$MBPS")" | |
| PER_STREAM_MBPS+=("$MBPS_FMT") | |
| done | |
| if $VERBOSE; then | |
| detail "📊 Per-stream speeds:" | |
| for m in "${PER_STREAM_MBPS[@]}"; do | |
| detail " - ${m} Mbps" | |
| done | |
| fi | |
| TOTAL_MBPS="$(calc "$TOTAL_BPS*8/1000000")" | |
| TOTAL_MBPS_FMT="$(two_dec "$TOTAL_MBPS")" | |
| # --------------------------- Output modes ---------------------- | |
| if $SMOKEPING; then | |
| # Print one numeric Mbps per stream, no labels/units, one per line. | |
| # SmokePing will read STDOUT lines and compute stats. | |
| for m in "${PER_STREAM_MBPS[@]}"; do | |
| printf '%s\n' "$m" | |
| done | |
| exit 0 | |
| fi | |
| # Default/verbose final line to STDOUT (no emojis): | |
| # "(<city>, <CC>) <hostname> — <XX.XX> Mbps" | |
| LOC_PREFIX="" | |
| if [[ -n "${FASTEST_CITY:-}" && -n "${FASTEST_CC:-}" ]]; then | |
| LOC_PREFIX="(${FASTEST_CITY}, ${FASTEST_CC}) " | |
| elif [[ -n "${FASTEST_CC:-}" ]]; then | |
| LOC_PREFIX="(${FASTEST_CC}) " | |
| fi | |
| printf '%s%s — %s Mbps\n' "$LOC_PREFIX" "$FAST_HOST" "$TOTAL_MBPS_FMT" | |
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment