Blacksec

Administrator
Staff member
ROOT
VIP
lqdajo.png


A website checker is a mass status tool: feed it a list of URLs, it tells you which are alive, which are dead, which are behind challenges, and which are lying about their status through redirect chains and soft-404s. The naive version — fire a GET, read the status code, write live or dead — produces garbage at scale, because live/dead is not a status-code question: a 200 response on a parked-domain placeholder page is dead in every way that matters, a 403 from a WAF challenge is alive but protected, a 301 chain ending in a 200 through three hops needs all three hops resolved to judge the final destination, and a slow origin timing out at 10 seconds might be alive at 30. This is website-checker making decoded: the status semantics that actually classify live versus dead, the redirect and timeout discipline that keeps mass runs honest, the thread-pool and proxy architecture behind 10,000-URL batches, and the deployment stack (Flask or PHP front-ends with async back-ends) that open tooling consistently lands on.

ROUTE MAP (PLAIN ENGLISH)

The tool's job is classification, and the classification has more classes than live/dead. Alive (clean): a final 2xx after redirect resolution, response body above the parked-page threshold, server headers present. Alive-protected: 403/401/429 or a challenge-page signature (Cloudflare's cf-chl, Incapsula, PerimeterX markers) — the site is up, access is gated; conflating this with dead is the single most common mass-checker error because it sends perfectly good domains to the dead file. Redirected-alive: one or more 3xx hops resolving to a live final destination — the record carries the full chain so the checker's consumer sees where the URL actually lands (critical when checking redirector URLs or shortened links). Dead: connection refused, NXDOMAIN, definitive 404, or a 200 that fingerprints as parked (domain-squatter landing pages, "this domain is for sale" markers, empty-template signatures). Soft-dead: 5xx persisting past retry, origin timeouts past the retry budget, TLS handshake failures — likely dead or broken rather than parked, sorted separately because broken origins sometimes recover and parked domains never do. Unknown: network-layer errors from the checker's own side (local DNS failures, proxy-died mid-request) parked for re-run, never classified as dead on the tool's own failure.

STEP 1 - STATUS SEMANTICS THAT ACTUALLY WORK

  • Method choice: HEAD first (half the bytes, the status and headers without the body), fall back to GET on targets that reject HEAD or return inconsistent HEAD/GET statuses — some origins 405 HEAD and 200 GET, some CDNs lie on HEAD; the two-step approach keeps mass throughput high while keeping classification honest for the minority of targets that misbehave on HEAD.
  • Status-code mapping: 2xx → alive-candidate (awaiting body check), 3xx → follow (up to a hop cap, typically 5-10, with loop detection on repeated locations), 401/403 → alive-protected (record the auth-scheme header), 404/410 → dead, 429 → alive-rate-limited (site up, checker throttled — re-run class with backoff, not dead), 5xx → soft-dead-after-retry (one retry on a fresh connection before classifying), 000-class connection failures → refused (dead) or timeout (soft-dead-after-retry) by error type.
  • Body-level truth: a 200 with a body under a threshold (a few hundred bytes) or matching parked-domain signatures ("domain is for sale", squatter-landing markers, template-empty heuristics) classifies parked-dead despite the 200 — the parked-page check is what stops domain-lists full of expired registrations from reading as fully alive; body checks only run on GET fallbacks, since HEAD carries no body.
  • Challenge-page classification: Cloudflare, Incapsula, and PerimeterX challenge signatures (known marker strings and header patterns like cf-mitigated, server-challenge) classify alive-protected — the domain resolves, the origin answers, the edge is refusing unauthenticated clients; this class feeds its own output file because the consumer's next step (real browser, different network, timing) differs from dead-domain handling.

STEP 2 - THE MASS ARCHITECTURE

  • Async worker model: an async HTTP client with a worker pool — not thread-per-URL (10,000 threads is a fantasy), but an async loop with a concurrency cap (hundreds of simultaneous in-flight requests per process, tuned against target politeness and the checker's own bandwidth); the queue drains the input list, each worker runs the two-step method flow (HEAD, GET fallback), classifies, writes to its class file with minimal lock contention (per-class buffered writers, not per-line file locks).
  • Timeout budget: connect timeout and read timeout as separate knobs — connect around 5-10 seconds, read 10-30 depending on target class; the timeout choice directly decides soft-dead volume: aggressive timeouts flood the soft-dead file with slow-but-alive sites, lenient timeouts stall the queue behind dead origins; the two-timeout split lets connect-failures classify fast (dead/refused) while slow-reading origins get their full read budget.
  • Retry policy: one retry on timeout and 5xx classes only, on a fresh connection; never retry 2xx/3xx/4xx classifications (they already have ground truth) and never retry through the same dead proxy (network-layer failures re-run on the worker's next proxy binding); the retry budget is what separates soft-dead (tried twice, still failing) from unknown (tool-side failure, re-run).
  • Proxy and egress strategy: mass checking through shared egress gets rate-limited (429-class) or IP-banned at edge CDNs fast; the working patterns: residential proxy pool with per-worker binding for sensitive targets, direct egress with per-target pacing for politeness-sensitive batches, and domain-grouped scheduling (same-host URLs batched through the same worker so the checker's requests to one host look like a single visitor session, not a distributed hammer).

STEP 3 - OUTPUT RECORDS AND CONSUMABILITY

  • Record shape: one line per URL carrying the full classification story — final URL after redirects, redirect chain count and hop list, final status code, classification class, response time, content-length where available, server header, challenge-type marker where protected, retry count; a consumer filtering live.txt needs the final URL (not the input URL when redirects moved), and a consumer debugging a soft-dead needs the response time and error type, which is why thin records ("url: dead") are useless at scale.
  • Class files: alive.txt, protected.txt, redirected.txt (with chains), dead.txt, parked.txt, softdead.txt, ratelimited.txt, unknown.txt — the class taxonomy from step one mapped to one file each; unknown.txt gets re-run automatically after proxy-pool rotation, which usually drains it to near-zero on the second pass.
  • Progress and rate surface: live counters (processed, per-class tallies, requests/second, current error streaks) — the operator surface that catches systematic failures in real time (a sudden 100% timeout streak means the egress died, not the internet); run reports at completion (totals per class, average latency per class, per-proxy contribution) feed the next run's tuning.
  • Dedup and normalization: input URLs normalized before queueing (scheme defaulting, trailing-slash consistency, case-normalized hosts, fragment stripping) with duplicate collapse — the same URL appearing in five list variants otherwise produces five identical classifications and five x'd record count, which corrupts downstream statistics that assume one record per unique target.

STEP 4 - THE DEPLOYMENT STACKS THAT SHIP

  • Flask pattern: Python async back-end (asyncio HTTP client) behind a Flask front-end — upload field for the URL list, config knobs (timeouts, concurrency, proxy toggle) as form controls, progress via server-sent events or polling, results downloadable as class-file bundles; the pattern dominates open tooling because the classification logic and the HTTP client live in the same language and the front-end is a thin shell.
  • PHP/XAMPP pattern: PHP back-end with multi-curl (curl_multi handles the async layer in PHP-land) — the zero-install local pattern (XAMPP, drop folder in htdocs, browser open) that keeps the tool fully offline; multi-curl's callback model handles classification as each transfer completes; the pattern's ceiling is lower concurrency discipline than the Python async stacks, but for 1,000-5,000 URL batches it ships and runs anywhere PHP runs.
  • CLI-first pattern: no front-end, batch runner with flags (input file, output dir, concurrency, proxy file, timeout) — the pattern that survives automation best (cron runs, pipeline integration, headless servers) — the web front-ends wrap the same core; authoring the core as a library first and the front-ends as shells keeps classification logic in one place instead of drifting between web and CLI versions.
  • Deployment hygiene: rate defaults that respect targets (modest concurrency, honest user agents, the two-step HEAD-then-GET flow), output directories timestamped per run, config file for persisted knobs (proxy provider, timeouts, class thresholds) — the difference between a tool that gets used repeatedly and a toy that runs once.

WORKED EXAMPLE - A 5,000-URL BATCH

Input: 5,000 URLs, a mixed list of domain names and full URLs with heavy duplication across formats (http/https variants, trailing slashes, some shortened links). Normalization and dedupe collapse to 4,180 unique targets. Worker pool: 200 concurrent connections, 8-second connect timeout, 15-second read timeout, one retry on timeout/5xx. First pass results: 2,940 alive-clean (final 2xx with healthy body lengths), 412 alive-protected (mostly Cloudflare challenge signatures — their own file with cf-marker details), 187 redirected-alive (chains of 1-4 hops, final URLs recorded), 390 dead (connection refused or NXDOMAIN — the domain-list expired-registrations fraction), 145 parked-dead (200s fingerprinting as squatter pages — would have read alive in a status-code-only checker), 78 soft-dead (timeouts twice through), 28 unknown (proxy mid-request deaths — re-run after pool rotation drains to 3 on the second pass). Average latency: 840ms alive-clean, 9.2s soft-dead. The run report's one actionable line: protected-class concentrated on one CDN's edge range, meaning the batch's egress IPs drew the CDN's reputation eye — next run rotates egress before the protected-class inflates further. Total wall time: 14 minutes for the full batch with classification-complete records in every class file, final URLs where redirects moved, chains preserved.

WORKING SEQUENCE

Code:
website checker: normalize+dedupe input -> HEAD first, GET fallback on 405/inconsistent -> classify: 2xx alive-candidate (body check vs parked signatures), 3xx follow (hop cap + loop detect, chain recorded), 401/403/challenge-sig = alive-protected, 404/410 dead, 429 rate-limited re-run class, 5xx/timeout soft-dead after one fresh retry, conn-refused dead, local errors unknown (never dead) -> async worker pool, two-knob timeouts (connect vs read), per-worker proxy binding, domain-grouped scheduling -> class files + full records (final URL, chain, latency, markers) -> unknown re-run after egress rotation

QUICK ANSWERS

  • Why does a simple status-code checker produce garbage at scale? Because live/dead is not a status-code question — 200s on parked squatter pages are dead, 403s and challenge pages are alive-protected, redirect chains need resolution to judge the final destination, and tool-side network errors are unknown rather than dead; each of those classes needs its own handling downstream, which is why the classification taxonomy is the tool's real product and the status code is just one input to it.
  • What is a soft-404 and why does it matter? A 200 response whose body fingerprints as a parked-domain placeholder ("domain is for sale", squatter-landing markers, empty-template signatures) — the status code says alive, the content says dead; at scale, expired domain lists are full of these, and a checker without body-level parked detection reports near-100% alive on lists that are mostly dead domains, which poisons every consumer decision downstream.
  • HEAD or GET for mass checking? HEAD first — half the bytes, carries the status and headers needed for most classifications — with automatic GET fallback on targets that reject HEAD or return inconsistent HEAD/GET statuses (some origins and CDNs misbehave on HEAD); the two-step flow keeps throughput high while keeping classification honest for the minority of misbehaving targets.
  • How should timeouts be configured? Connect and read as separate knobs: connect 5-10 seconds (decides refused-vs-dead fast), read 10-30 seconds (gives slow-reading alive origins their budget); aggressive single timeouts flood the soft-dead file with slow-but-alive sites, lenient ones stall the queue behind dead origins — the split plus one fresh-connection retry on timeout/5xx is what cleanly separates soft-dead from unknown.
  • Why do challenge pages need their own class? A Cloudflare or Incapsula challenge means the domain resolves and the origin answers — the site is alive, the edge is refusing unauthenticated clients; conflating protected with dead sends good domains to dead-file consumers (who drop or replace them), and the correct downstream action (real browser, different egress, timing the retry) is completely different from dead-domain handling, which is why the challenge markers get extracted into the protected record.
  • What concurrency is safe for mass checking? Async worker pools with a concurrency cap (hundreds in-flight per process, not thread-per-URL), per-target politeness via domain-grouped scheduling (one host's URLs batched through one worker so the checker looks like a single visitor, not a distributed hammer), and egress strategy matching target sensitivity (residential proxies for CDN-protected batches, direct with pacing for politeness-sensitive targets) — 429-class spikes mid-run mean egress reputation, not target death, and the re-run class catches it.

RELATED