Download the PHP package edulazaro/laradomains without Composer
On this page you can find all versions of the php package edulazaro/laradomains. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download edulazaro/laradomains
More information about edulazaro/laradomains
Files in edulazaro/laradomains
Package laradomains
Short Description Everything you can learn about a domain without visiting it: parsing with the Public Suffix List, RDAP registration data, age, DNS over HTTPS and malware or adult screening
License MIT
Informations about the package laradomains
Laradomains
Everything you can learn about a domain without visiting it: parsing with the Public Suffix List, registration data over RDAP, age, DNS over HTTPS and malware or adult screening. Every answer comes from a third party (the registry, a DNS resolver, the Wayback Machine), never from the domain itself, which matters when the domain was typed by a stranger and may be hostile.
Installation
Requires PHP 8.4+ with the intl extension and Laravel 12 or newer. Optionally publish the config:
Parsing
Domain::parse() takes what people paste and keeps the host, lower-cased, in both spellings:
The registrable domain follows the Public Suffix List, not a guess at "the last two labels", so gob.es, com.mx and wildcard rules are handled. Suffixes run by hosting platforms are opt-in:
www is kept, since it is a host of its own; withoutWww() drops it when you treat both as one site.
The host is read the way a browser reads it, which is what matters when deciding where a link really goes: a backslash ends the host as a slash does, so https://evil.example\@paypal.com/login is evil.example, not PayPal, and a web scheme without slashes (http:evil.example) still names its host.
Lookalikes
skeleton() gives the Latin reading of a name. imitates() checks it against the brands you care about, as a copy of the whole name (0/o and 1/l swaps included); impersonates() adds the usual phishing shapes, the brand as a label or a hyphenated part of someone else's domain:
typosquats() catches the typing slips: a letter added (paypall.com), dropped (payal.com), swapped with its neighbour (paypla.com), a lookalike letter (goog1e.com) or two letters that read as one (arnazon.com). Any other replaced letter does not count, since paypay.com is a real company one letter from PayPal, and brand names under five letters are skipped:
The kinds of slip do not weigh the same, and typosquat() says which one it was. A lookalike letter or a pair read as one (goog1e, arnazon) is rarely an accident; letters swapped (paypla) is a medium sign; a letter added or dropped (paypall, payal) is the weakest, and where ordinary words land, since no code knows without a dictionary that apples is a word:
Brand names under four letters only match impersonates() as full copies, since bbc or x turn up inside ordinary names. A brand that is also a common word will still match ordinary sites (apple.com matches apple-pie-recipes.com), so treat impersonates() as a reason to review a link, not to block it; imitates(), a copy of the whole name, is the stronger signal.
Keeping the lists current
The package ships a copy of the Public Suffix List and of the IANA RDAP bootstrap, so it works with no download and no cache. laradomains:update fetches fresh copies to storage/app/laradomains/, which win over the bundled ones, and compiles the suffix list to a PHP array that opcache keeps in memory. Schedule it monthly:
A download that does not look like the real list is discarded and the current copy kept.
Registration data (RDAP)
RDAP is the protocol that replaced WHOIS. There is no central database: each registry runs its own server, and IANA publishes which one serves each TLD. Laradomains reads that bootstrap from disk and asks the registry directly, for the registrable domain, so a lookup is a single request:
Three outcomes are kept apart, because a caller handles each one differently:
| Meaning | |
|---|---|
supported === false |
The TLD has no RDAP service (.es, .de, .io and others). |
registered === false |
The registry answered that nobody holds the name. |
failed() |
The lookup did not complete; error says why. The only one worth retrying. |
TLDs missing from the IANA bootstrap are sent to a relay, rdap.org by default. Set LARADOMAINS_RDAP_FALLBACK= empty to never use one.
Age
age() returns null both when there is no date to give and when a lookup failed. ageCheck() takes the same arguments and tells them apart:
| Status | Reason | Meaning |
|---|---|---|
found |
A date. failed may still list sources that did not answer. |
|
none |
unsupported_tld |
The TLD's registry publishes no RDAP, and no fallback was asked for. |
none |
not_registered |
The registry has no data for that name (an RDAP 404). Some registries implement RDAP badly, so this is not proof that the name is free. |
none |
no_registration_date |
The registry answered, without a registration date. |
none |
no_certificates, no_captures |
The fallbacks answered, with nothing. With both asked, the reason is the last one asked (no_captures). |
unknown |
lookup_failed |
A source did not answer and none found a date. Worth asking again later. |
Any date is found, even when another source failed: when RDAP is down and crt.sh answers, the result is a date from certificates with failed: ["rdap"]. A date from certificates or the Wayback Machine is a lower bound, so check failed before reading it as "registered on". The fallbacks are only asked when RDAP gives no date, so an empty failed with source rdap means nobody else needed asking, not that everyone was asked.
Several registries (.es, .de, .io, .it) publish no RDAP, so their domains have no registration date. Two lower bounds stand in for it, both opt-in, and with both asked the earliest date wins:
certificates: trueasks crt.sh for the first certificate in the Certificate Transparency logs. Every publicly trusted certificate is logged, and a phishing domain usually gets one the day it is registered, so this is what answers "is it new?" for those TLDs. A domain that never used HTTPS has none, and crt.sh is a free service that is not always up: a failure is unknown, never new. The answer is read as a stream, up tocertificates.max_bytes(2 MB): any certificate proves the domain existed on its date, so the earliest one read is a valid lower bound even if the rest is cut off, and a domain with megabytes of certificates is years old anyway. That keeps it safe on a request path with a short timeout.wayback: trueasks for the first Wayback Machine capture, which sees old domains well and new ones hardly at all.
The Wayback fallback is off by default: its CDX server allows about a dozen requests a minute, which suits a queued job and not a request path. A refused request waits wayback.retry_delay milliseconds (5000 by default) before each of wayback.retries retries. A first capture is a lower bound, not a registration date.
To remember ages, set LARADOMAINS_AGE_CACHE_FOR to a number of seconds. Age belongs to the registrable domain, so a.spam.io and b.spam.io share one entry, and the whole check is kept (status, reason and failed sources). Complete answers (found or none with every source answering) are kept for cache_for; unknown, and found with a failed source, for age.retry_after seconds (300 by default), so a spam campaign repeating the same new domain costs one registry request, not one per message. Nothing is cached in an array or null store, which would not survive the request; LARADOMAINS_CACHE_STORE picks another store.
DNS
Over HTTPS by default (LARADOMAINS_DNS_DRIVER=doh), so answers do not depend on the resolver of the machine; system uses dns_get_record().
acceptsMail() decides the way a mail server does (RFC 5321 §5.1): an MX host accepts mail; a null MX (0 ., RFC 7505) refuses it; with no MX at all, mail goes to the domain's own A or AAAA address, and a domain with neither (or that does not exist) refuses it. When a lookup fails (timeout, HTTP error, SERVFAIL) the answer is null, never false: a disposable-address check must not reject a real domain because a resolver hiccuped. The other methods describe rather than decide, so a failed lookup there is an empty list.
Screening
Cloudflare's filtering resolvers answer 0.0.0.0 for names they block. Laradomains asks them over DNS, so nothing reaches the domain:
Both resolvers are asked in parallel, so the wait is the slower of the two. It fails closed: when a resolver cannot be asked (timeout, HTTP error, SERVFAIL) the answer is Screen::UNKNOWN, never CLEAN. "Could not look" and "found nothing" are different answers, and what UNKNOWN means (let through, hold for review, retry later) is the caller's decision. A name that does not exist (NXDOMAIN) is an answer, and clean.
Domains::verdict() keeps the two answers apart, so one resolver failing does not hide what the other settled:
The family resolver blocks malware as well as adult content, so its block alone means "malware or adult": decision() says adult only when the malware resolver has cleared the domain, and unknown otherwise. blocked() is true as soon as any resolver blocked it, for a caller that only needs to know whether to stop the link. Screen::isMalware() asks only the malware resolver and answers true, false, or null when it could not check.
To cache verdicts per host, set LARADOMAINS_SCREEN_CACHE_FOR in seconds: complete verdicts are kept that long, incomplete ones for screen.retry_after (60 by default), in a persistent store.
The adult category is broad (it also catches piracy, cannabis shops and the odd false positive): treat it as a flag for review rather than as proof.
Timeouts and retries
Each service has its own timeout and retries, so a request path is not held by the slowest one:
Every network call also takes its own timeout (seconds) and retries (tries after the first; null takes the config, 0 makes a single attempt): rdap(), age(), ageCheck(), screen(), verdict(), Screen::isMalware(), DnsClient::query(), queryEach(), acceptsMail(), DomainAge::firstCertificate() and firstCapture().
The timeout applies to each request, so the worst case of one request is (retries + 1) × timeout + retries × retry_delay. With the defaults:
| Service | Worst case per request |
|---|---|
rdap |
1 × 10 s = 10 s |
dns |
2 × 3 s + 0.2 s = 6.2 s (acceptsMail() can make three: MX, A, AAAA) |
screen |
1 × 2 s = 2 s (both resolvers in parallel) |
certificates |
2 × 20 s + 3 s = 43 s |
wayback |
2 × 20 s + 5 s = 45 s |
ageCheck() adds up its sources: RDAP, then the fallbacks asked for. The defaults suit a queued job. In a web request, use a short timeout and no retries, and treat null or unknown as "check later":
Rate limits
Every request passes through a hook first, with the service name (rdap, dns, screen, wayback, certificates). It is the place for a rate limiter shared across workers:
Testing
Every call goes through Laravel's HTTP client, so Http::fake() covers the whole package:
Sponsors
Laradomains is supported by the following sponsors. Thank you for keeping it growing:
Kenodo AndorraDev Valor Web ToxicFilter
Author
Created by Edu Lazaro
License
Laradomains is open-sourced software licensed under the Mozilla Public License 2.0.