Skip to content

About

A single-file, dependency-free Python CLI that follows a URL through every redirect hop and shows you exactly what happened along the way — status codes, timing, TLS, cookies, and a set of built-in security checks for shady redirect chains.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

🔍 Redirect Detector

Follow a URL through every redirect hop — status codes, timing, TLS, cookies, and built-in security checks.

Python Dependencies License Platform

Single file · Zero dependencies · Standard library only


Table of Contents

Why

Shortened and redirected links hide what they actually do. Redirect Detector unwinds the whole chain so you can see it:

  • 🌍 Where a link really goes
  • 🖥️ What infrastructure serves each hop
  • 🔓 Whether it silently drops encryption
  • ⚠️ Whether anything about the chain looks off — before you (or someone you sent it to) follows it

Features

Per-hop detail

  • HTTP status code with a human-readable label
  • HTTP method, and how it changes across redirect types (301/302/303 → GET, 307/308 preserve method + body)
  • Resolved IP address, port, and Server header for the actual backend that answered
  • Timing breakdown: DNS lookup / TCP+TLS connect / time-to-first-byte
  • TLS certificate issuer and expiry on HTTPS hops
  • Cookies set at each hop
  • Known URL-shortener / tracker-domain labeling (bit.ly, t.co, doubleclick.net, …) — user-extensible
  • Retry-After guidance on 429/503 responses

🔐 Security notices

Notice What it catches
🔓 Downgrade HTTPS → HTTP mid-chain
🔁 Loop Redirect loops
🎯 Internal IP Redirects to private/loopback/link-local IPs or cloud metadata (169.254.169.254) — a common SSRF/redirect-abuse pattern
📜 Weak TLS Expired certificates, or expiring within 14 days
🍪 Unsecured cookies Cookies missing the Secure flag over HTTPS, or missing HttpOnly
➰ Long chains Unusually long redirect chains — a common cloaking/phishing signal
⚠️ Hop limit Redirects that hit the configured hop limit before resolving

Summary view

  • One-line chain diagram (host ─301─▶ host ─302─▶ host)
  • Redirect-type breakdown (301 vs 302 vs 307/308, etc.)
  • Every unique domain, IP, and server seen in the chain
  • An equivalent curl command for reproducing the initial request by hand
  • Best-effort HTTP/2 (ALPN) support probe on the final destination — run as a fully isolated side probe, so a middlebox that mishandles the h2 ALPN offer can never break the main trace

Modes

  • --json — machine-readable output for scripting, CI, and piping into jq
  • --diff-user-agent — run the trace twice with different User-Agents and report where the chains diverge (catches mobile-vs-desktop or bot-vs-browser redirect branching)
  • --method / --data — trace a non-GET initial request (e.g. where a webhook POST ends up)
  • --insecure — skip TLS verification against self-signed or internal endpoints
  • --output FILE — write the trace straight to disk

Example Output

🔍 Tracing redirects for: http://bit.ly/example
────────────────────────────────────────────────────────────────────────────
  Hop 1: GET http://bit.ly/example
    → 301 redirect to: https://example.com/landing?utm_source=bitly
  Hop 2: GET https://example.com/landing?utm_source=bitly
    → 200 (94ms)

REDIRECT TRACE
────────────────────────────────────────────────────────────────────────────
├─ Hop 1 [301] GET  118ms [shortener]
│  http://bit.ly/example
│  Moved Permanently → following redirect
│  Host  bit.ly:80
│  IP    67.199.248.10
│  Time  dns 12ms / connect 19ms / ttfb 60ms
│
└─ Hop 2 [200] GET  94ms
   https://example.com/landing?utm_source=bitly
   OK — Final destination
   Host  example.com:443
   IP    93.184.216.34
   TLS   DigiCert Inc (expires Nov 03 12:00:00 2026 GMT)
   Time  dns 4ms / connect 8ms / tls 22ms / ttfb 41ms
   Cookie session_id

✓ FINAL DESTINATION REACHED
  Total hops:    2
  Final status:  200
  Final URL:     https://example.com/landing?utm_source=bitly
  Protocol:      HTTP/2
  Total time:    212ms

  Chain:
    bit.ly ─301─▶ example.com

ℹ Notes:
  • hop 1: bit.ly is a known shortener

Requirements

  • Python 3.8+ — that's it.
  • No third-party dependencies. Everything runs on the standard library (urllib, http.client, ssl, socket). No venv, no pip install, no lockfile.

Installation

curl -O https://raw.githubusercontent.com/Lunatic16/redirect-detector/main/redirect_detector.py
chmod +x redirect_detector.py

Tip

Drop it somewhere on your $PATH to run it as redirect-detector from anywhere:

mv redirect_detector.py ~/.local/bin/redirect-detector

Usage

python3 redirect_detector.py <url> [options]

If you omit the scheme, the tool probes both https:// and http:// and picks whichever responds:

python3 redirect_detector.py bit.ly/example

Options

Flag Short Description
--headers -H Show response headers for each hop
--timeout SECONDS -t Request timeout per hop (default: 15)
--user-agent UA -a Custom User-Agent string
--max-hops N -m Maximum redirects to follow before stopping (default: 30)
--no-ip Skip DNS resolution / IP display for a faster trace
--json Machine-readable JSON output instead of the formatted trace
--no-color Disable ANSI colors
--curl Print an equivalent curl command for the initial request
--no-http2-probe Skip the best-effort HTTP/2 probe on the final URL
--diff-user-agent UA Re-run the trace with a second User-Agent and report where the chains diverge
--method METHOD -X HTTP method for the initial request (default: GET)
--data BODY -d Request body for the initial request (implies POST if --method isn't set); dropped on a 301/302/303 method-reset, carried through 307/308
--insecure -k Skip TLS certificate verification
--output FILE -o Write the trace (formatted or --json) to a file instead of stdout
--extra-shorteners DOMAINS Comma-separated extra domains to treat as shorteners for this run
--extra-trackers DOMAINS Comma-separated extra domains to treat as trackers for this run

Run --help for the full built-in reference at any time.

Examples

# Basic trace
python3 redirect_detector.py https://bit.ly/example

# Show response headers at every hop
python3 redirect_detector.py https://t.co/abc123 --headers

# Skip DNS lookups for a faster trace
python3 redirect_detector.py https://example.com --no-ip

# Machine-readable output, piped into jq
python3 redirect_detector.py https://example.com --json | jq '.final_url'

# Get the equivalent curl command
python3 redirect_detector.py https://example.com --curl

# Compare how a link behaves for two different User-Agents
python3 redirect_detector.py https://example.com --diff-user-agent "curl/8.0"

# Trace where a webhook POST redirect actually ends up
python3 redirect_detector.py https://example.com -X POST --data "event=ping"

# Trace a self-signed internal endpoint
python3 redirect_detector.py https://internal.example.local --insecure

# Save a JSON trace to disk
python3 redirect_detector.py https://example.com --output trace.json --json

Extending the Shortener/Tracker List

The built-in shortener and tracker lists cover the common cases but aren't exhaustive. Add your own domains permanently via a config file:

~/.config/redirect_detector/domains.txt
# one entry per line — '#' starts a comment
shortener:mycompany.link
tracker:some-ad-network.example

…or one-off for a single run:

python3 redirect_detector.py https://example.com --extra-shorteners "mycompany.link,short.io"

JSON Output

--json emits a single object per trace:

{
  "requested_url": "http://bit.ly/example",
  "hops": [ /* one object per hop — status, method, host, ip, port, server,
               timing, tls, cookies, category, retry_after, ... */ ],
  "failures": [],
  "warnings": [],
  "notes": ["hop 1: bit.ly is a known shortener"],
  "final_status": 200,
  "final_url": "https://example.com/landing",
  "total_hops": 2,
  "total_ms": 212.4,
  "redirect_type_breakdown": { "301": 1 },
  "final_protocol": "HTTP/2"
}

Exit Codes

Useful for scripting and CI checks:

Code Meaning
0 Final destination reached with a 2xx status
1 Chain resolved, but the final status wasn't 2xx (e.g. ended on a 404)
2 The trace never produced a usable response — connection failure, timeout, or an unparseable URL
python3 redirect_detector.py https://example.com --no-color >/dev/null 2>&1 \
  || echo "link is broken"

Design Notes

  • Single file, zero dependencies. Works anywhere Python 3.8+ does — no venv, no pip install, no lockfile.
  • Clean stdout/stderr split. Progress logging goes to stderr; stdout carries only the final trace, so --json output pipes cleanly into jq or a file even while you watch progress live.
  • Isolated HTTP/2 detection. The HTTP/2 probe is a separate ALPN check against only the final URL, so a middlebox that mishandles the h2 offer can never affect redirect-following itself.
  • Never trusts a URL to be well-formed. Both the input URL and every Location header along the chain are treated as potentially malformed; parsing failures degrade to a clean error instead of a crash.

Contributing

Issues and pull requests are welcome! Some ideas that would fit well:

  • More domains for the built-in shortener/tracker lists
  • Additional security checks (open redirects, redirect chains across suspicious TLDs, …)
  • Windows TERM/color edge cases and bug fixes

License

Distributed under the MIT License.

About

A single-file, dependency-free Python CLI that follows a URL through every redirect hop and shows you exactly what happened along the way — status codes, timing, TLS, cookies, and a set of built-in security checks for shady redirect chains.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages