Veruse / Redirect Checker

Last active 1 month ago

Like 0

bash <(curl -fsSL https://gist.veru.se/Veruse/87653b8402024210986ecec3c826e1b9/raw/HEAD/redirect-checker.sh)

Revision a3eecbc916126c8cac74277b53d2b02d47fcaef2

redirect-checker.sh Raw
1#!/usr/bin/env bash
2#
3# check_redirects.sh - Follow and report HTTP redirect chains (max 10 hops).
4#
5# Run straight from a gist (it will ask for the URLs):
6# bash <(curl -fsSL https://gist.veru.se/Veruse/87653b8402024210986ecec3c826e1b9/raw/HEAD/redirect-checker.sh)
7# curl -fsSL https://gist.veru.se/Veruse/87653b8402024210986ecec3c826e1b9/raw/HEAD/redirect-checker.sh | bash
8#
9# Arguments/flags still work if you want to skip the prompts:
10# bash <(curl -fsSL URL) https://a.com https://b.com
11# bash <(curl -fsSL URL) -f urls.txt
12
13set -uo pipefail
14
15MAX_REDIRECTS=10
16TIMEOUT=10
17CSV=0
18URL_FILE=""
19USER_AGENT="Mozilla/5.0 (redirect-checker)"
20
21if [[ -t 1 ]]; then
22 RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; CYAN=$'\e[36m'; BOLD=$'\e[1m'; RESET=$'\e[0m'
23else
24 RED=""; GREEN=""; YELLOW=""; CYAN=""; BOLD=""; RESET=""
25fi
26
27usage() {
28 cat <<'EOF'
29Usage: check_redirects.sh [options] [url ...]
30 (no URLs given = interactive prompts)
31
32Options:
33 -f FILE Read URLs from FILE (one per line, # comments and blank lines ignored)
34 -m N Max redirects to follow (default: 10)
35 -t SECS Timeout per request in seconds (default: 10)
36 -c Output CSV (url,final_url,final_status,hops,result)
37 -h Show help
38EOF
39 exit "${1:-0}"
40}
41
42while getopts ":f:m:t:ch" opt; do
43 case "$opt" in
44 f) URL_FILE="$OPTARG" ;;
45 m) MAX_REDIRECTS="$OPTARG" ;;
46 t) TIMEOUT="$OPTARG" ;;
47 c) CSV=1 ;;
48 h) usage 0 ;;
49 :) echo "Option -$OPTARG requires an argument." >&2; usage 1 ;;
50 \?) echo "Unknown option: -$OPTARG" >&2; usage 1 ;;
51 esac
52done
53shift $((OPTIND - 1))
54
55command -v curl >/dev/null 2>&1 || { echo "Error: curl is required." >&2; exit 1; }
56
57# ---- Collect URLs ----
58URLS=()
59if [[ -n "$URL_FILE" ]]; then
60 [[ -r "$URL_FILE" ]] || { echo "Error: cannot read $URL_FILE" >&2; exit 1; }
61 while IFS= read -r line || [[ -n "$line" ]]; do URLS+=("$line"); done < "$URL_FILE"
62fi
63URLS+=("$@")
64
65# ---- Interactive prompts (read from the terminal, so it works with curl | bash) ----
66TTY="${TTY_DEVICE:-/dev/tty}"
67if [[ ${#URLS[@]} -eq 0 ]]; then
68 if ! { exec 3<"$TTY"; } 2>/dev/null; then
69 echo "Error: no URLs given and no terminal available to ask for them." >&2
70 usage 1
71 fi
72
73 echo "${BOLD}Redirect checker${RESET} (max $MAX_REDIRECTS redirects per URL)"
74 echo " 1) Check a single URL"
75 echo " 2) Paste a list of URLs"
76 echo " 3) Load URLs from a file"
77 read -r -u 3 -p "Choose [1-3]: " choice
78
79 case "$choice" in
80 1)
81 read -r -u 3 -p "URL: " line
82 URLS+=("$line")
83 ;;
84 2)
85 echo "Paste URLs, one per line. Press Enter on an empty line when done:"
86 while IFS= read -r -u 3 line; do
87 line="${line//$'\r'/}"
88 [[ -z "${line// /}" ]] && break
89 URLS+=("$line")
90 done
91 ;;
92 3)
93 read -r -u 3 -p "File path: " URL_FILE
94 URL_FILE="${URL_FILE/#\~/$HOME}"
95 [[ -r "$URL_FILE" ]] || { echo "Error: cannot read $URL_FILE" >&2; exit 1; }
96 while IFS= read -r line || [[ -n "$line" ]]; do URLS+=("$line"); done < "$URL_FILE"
97 ;;
98 *)
99 echo "Invalid choice." >&2; exit 1 ;;
100 esac
101
102 read -r -u 3 -p "Save results as CSV instead of on-screen output? [y/N]: " ans
103 if [[ "$ans" =~ ^[Yy] ]]; then
104 read -r -u 3 -p "CSV file name [redirects.csv]: " csvfile
105 csvfile="${csvfile:-redirects.csv}"
106 CSV=1
107 echo "Writing results to $csvfile ..."
108 exec >"$csvfile"
109 fi
110 exec 3<&-
111fi
112
113[[ ${#URLS[@]} -eq 0 ]] && { echo "No URLs provided." >&2; exit 1; }
114
115csv_escape() { local s="${1//\"/\"\"}"; printf '"%s"' "$s"; }
116
117# ---- Check a single URL ----
118check_url() {
119 local start="$1" current="$1" hops=0 result="" code="" next=""
120 declare -A seen=()
121
122 [[ "$current" =~ ^https?:// ]] || current="http://$current"
123 [[ $CSV -eq 0 ]] && printf '\n%s%s%s\n' "$BOLD" "$start" "$RESET"
124
125 while :; do
126 local out
127 out=$(curl -s -o /dev/null --max-time "$TIMEOUT" -A "$USER_AGENT" \
128 -w '%{http_code} %{redirect_url}' "$current" 2>/dev/null)
129 code="${out%% *}"
130 next="${out#* }"; [[ "$next" == "$out" ]] && next=""
131
132 if [[ -z "$code" || "$code" == "000" ]]; then
133 result="ERROR (connection failed/timeout)"
134 [[ $CSV -eq 0 ]] && printf ' [%2d] %sERR%s %s\n' "$hops" "$RED" "$RESET" "$current"
135 break
136 fi
137
138 local colour="$GREEN"
139 [[ "$code" =~ ^3 ]] && colour="$YELLOW"
140 [[ "$code" =~ ^[45] ]] && colour="$RED"
141 [[ $CSV -eq 0 ]] && printf ' [%2d] %s%s%s %s\n' "$hops" "$colour" "$code" "$RESET" "$current"
142
143 seen["$current"]=1
144
145 # Not a redirect (or redirect without Location) -> done
146 if [[ ! "$code" =~ ^3 || -z "$next" ]]; then
147 if [[ "$code" =~ ^2 ]]; then result="OK"
148 elif [[ "$code" =~ ^3 ]]; then result="REDIRECT WITHOUT LOCATION"
149 else result="HTTP $code"; fi
150 break
151 fi
152
153 if [[ -n "${seen[$next]:-}" ]]; then
154 result="LOOP DETECTED"
155 [[ $CSV -eq 0 ]] && printf ' %s↻ loops back to %s%s\n' "$RED" "$next" "$RESET"
156 current="$next"; hops=$((hops + 1))
157 break
158 fi
159
160 hops=$((hops + 1))
161 current="$next"
162
163 if (( hops > MAX_REDIRECTS )); then
164 result="TOO MANY REDIRECTS (>$MAX_REDIRECTS)"
165 break
166 fi
167 done
168
169 if [[ $CSV -eq 1 ]]; then
170 printf '%s,%s,%s,%s,%s\n' "$(csv_escape "$start")" "$(csv_escape "$current")" \
171 "$code" "$hops" "$(csv_escape "$result")"
172 else
173 local rc="$GREEN"; [[ "$result" != "OK" ]] && rc="$RED"
174 printf ' %s→ %s%s | hops: %d | final: %s\n' "$rc" "$result" "$RESET" "$hops" "$current"
175 fi
176
177 [[ "$result" == "OK" ]]
178}
179
180# ---- Main ----
181[[ $CSV -eq 1 ]] && echo "url,final_url,final_status,hops,result"
182
183total=0; failed=0
184for raw in "${URLS[@]}"; do
185 url="$(echo "$raw" | tr -d '\r' | xargs)" # trim whitespace / CRLF
186 [[ -z "$url" || "$url" == \#* ]] && continue
187 total=$((total + 1))
188 check_url "$url" || failed=$((failed + 1))
189done
190
191if [[ $CSV -eq 0 ]]; then
192 printf '\n%sChecked %d URL(s): %d OK, %d with issues%s\n' "$CYAN" "$total" "$((total - failed))" "$failed" "$RESET"
193fi
194
195(( failed == 0 ))