Skip to content

Instantly share code, notes, and snippets.

@NF1198
Last active October 26, 2025 08:36
Show Gist options
  • Save NF1198/d0ffc5b510eb7a33651123771396f816 to your computer and use it in GitHub Desktop.
Save NF1198/d0ffc5b510eb7a33651123771396f816 to your computer and use it in GitHub Desktop.
Tests fast.com from bash
#!/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