Documentation
¶
Overview ¶
Package plat looks up ownership of a domain, IP address, or autonomous system. It queries RDAP and WHOIS concurrently -- from both registry and registrar for domains, from the responsible RIR for IPs and ASNs -- and merges what comes back into one record with per-field provenance: which source supplied each value, and where sources disagree.
Getting started ¶
Build a Client with New, then call Lookup for each name, address, or AS number:
c, err := plat.New(ctx, plat.Options{})
if err != nil {
// New rarely fails: a failed bootstrap fetch falls back to a
// cached copy and then to a snapshot embedded in the binary.
}
res, err := c.Lookup(ctx, "example.com")
Lookup classifies the input and returns a Result holding exactly one of Domain, IP, or ASN, matching Result.Kind.
Reuse the Client ¶
A Client holds the IANA RDAP bootstrap data, a per-server WHOIS pacing limiter, and a cache of IANA WHOIS referrals -- all worth keeping across calls. A program looking up many names should build one Client and reuse it for every Lookup, not construct one per name: doing the latter throws away the bootstrap fetch and, more importantly, resets the pacing limiter each time, so it can no longer see that a burst of lookups is hitting one WHOIS server repeatedly. A Client is safe for concurrent use, so this works from multiple goroutines too.
Pacing itself is on by default and is scoped per WHOIS server: it exists so a bulk run cannot hammer one server, and it costs nothing for a server a lookup only queries once. It is not free in every case -- if one lookup's own referral chain reaches the same WHOIS host twice (a registry that refers the registrar query back to itself, which is what example.com does), the second query waits out the full interval. Options.DisableWHOISPacing turns pacing off, which is the right choice for a program that only ever does one lookup at a time.
Provenance is the point ¶
Every field on Record, IPRecord, and ASNRecord is a Field[T], carrying both the merged value and the list of sources that supplied it. This is not incidental metadata -- it is the reason plat merges sources at all rather than just picking one. Where sources disagree beyond what merge tolerates (e.g. clock skew on timestamps), the field keeps its highest-precedence value and the disagreement is recorded in Conflicts, never silently dropped.
A failing source is not an error ¶
RDAP or WHOIS being unreachable for one source is normal, not exceptional: as long as at least one source returns data, Lookup returns a Result with a nil error, and the per-source detail -- including which sources failed and why -- lives in the record's Sources field. ErrNotFound and ErrLookupFailed are returned alongside a populated Result too, so a caller diagnosing either case still has that detail to inspect.
Internal types, public aliases ¶
Record, IPRecord, ASNRecord, and their component types are aliases to types defined in an internal package: the same type, so no conversion happens at the boundary, but the implementation underneath stays free to change without breaking anything built against this package.
Not yet exposed ¶
The JSON, human, and plain renderers plat's CLI uses are not part of this package. A caller wanting plat's documented "schemaVersion": 1 JSON output cannot currently produce it from a Result and must encode the fields it needs itself.
Stability ¶
This API is v0 and may change before 1.0.
Index ¶
Examples ¶
Constants ¶
const ( SourceRegistrarRDAP = model.SourceRegistrarRDAP SourceRegistryRDAP = model.SourceRegistryRDAP SourceRegistrarWHOIS = model.SourceRegistrarWHOIS SourceRegistryWHOIS = model.SourceRegistryWHOIS )
The four sources plat merges, in precedence order.
const ( LifecycleAutoRenewGrace = model.LifecycleAutoRenewGrace LifecycleRedemptionGrace = model.LifecycleRedemptionGrace LifecyclePendingRestore = model.LifecyclePendingRestore LifecyclePendingDelete = model.LifecyclePendingDelete )
The stages of ICANN's Expired Registration Recovery Policy (ERRP) timeline that LifecycleInfo.Stage can report.
Variables ¶
var ( // ErrInvalidInput means the input is not a name plat can look up -- // a single label, a reserved or private IP, and so on. It wraps the // specific cause, so errors.Is against it succeeds while the // underlying error stays inspectable. ErrInvalidInput = errors.New("plat: invalid input") // ErrNotFound means every source reported that the object does not // exist. It corresponds to the CLI's exit code 1. ErrNotFound = errors.New("plat: not found") // ErrLookupFailed means no source returned data and at least one // failed for a reason other than not-found. It corresponds to the // CLI's exit code 3. Inspect Result's per-source details for why. ErrLookupFailed = errors.New("plat: lookup failed") )
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client performs lookups. It holds the IANA bootstrap resolver, a per-server WHOIS pacing limiter, and a cache of IANA WHOIS referrals, all of which are worth reusing across lookups -- so a program looking up many names should create one Client and keep it.
A Client is safe for concurrent use.
func New ¶
New builds a Client, loading the IANA RDAP bootstrap data once.
It rarely fails: a failed fetch falls back to a cached copy and then to a snapshot embedded in the binary, so a caller with no network still gets a usable Client. The error return exists so that a future failure mode is not a breaking signature change.
func (*Client) Lookup ¶
Lookup classifies input as a domain, IP address, or ASN, queries the relevant RDAP and WHOIS sources concurrently, and merges the answers.
A source failing is normal and is not an error: as long as one source returned data, Lookup returns a Result with nil error, and the per-source detail is in the record's Sources field. Lookup returns ErrInvalidInput, ErrNotFound, or ErrLookupFailed for the three cases where there is no usable answer at all.
Example ¶
ExampleClient_Lookup shows the New/Lookup shape. It performs real network I/O, so it has no "// Output:" comment: godoc renders it, but go test does not execute it.
package main
import (
"context"
"errors"
"fmt"
"github.com/patramsey/plat"
)
func main() {
c, err := plat.New(context.Background(), plat.Options{})
if err != nil {
panic(err)
}
res, err := c.Lookup(context.Background(), "example.com")
switch {
case errors.Is(err, plat.ErrNotFound):
fmt.Println("no such object")
return
case err != nil:
panic(err)
}
switch res.Kind {
case plat.KindDomain:
fmt.Println(res.Domain.Expires.Value.Time, res.Domain.Expires.Sources)
case plat.KindIP:
fmt.Println(res.IP.CIDR.Value)
case plat.KindASN:
fmt.Println(res.ASN.Name.Value)
}
}
Output:
type Field ¶
Field is a single merged value together with the sources that supplied it. Per-field provenance is plat's central idea, not a decoration.
type LifecycleInfo ¶
type LifecycleInfo = model.LifecycleInfo
LifecycleInfo explains where an expired gTLD domain sits in ICANN's deletion timeline.
type LifecycleStage ¶
type LifecycleStage = model.LifecycleStage
LifecycleStage is a stage of that timeline.
type Options ¶
type Options struct {
// Timeout bounds the time one lookup spends talking to servers.
// Deliberate idling -- waiting a turn behind another name's paced
// WHOIS query -- is not charged against it. Zero means 5s.
Timeout time.Duration
// Sources restricts which sources are consulted. nil means all.
Sources []SourceID
// NoFollow skips the second hop to the registrar's RDAP server.
// Domain lookups only; IPs and ASNs have no registrar.
NoFollow bool
// CacheDir overrides where the IANA bootstrap file is cached. Empty
// means the OS user cache directory, the same place the CLI uses.
CacheDir string
// DisableCache stops plat reading or writing the filesystem at all.
DisableCache bool
// RefreshBootstrap forces a bootstrap fetch even when a fresh cached
// copy exists.
RefreshBootstrap bool
// WHOISInterval is the minimum spacing between WHOIS queries to any
// one server. Zero means whois.DefaultWHOISInterval. Pacing is
// always on, but costs a single lookup nothing: the first query to a
// given server is never delayed.
WHOISInterval time.Duration
// DisableWHOISPacing turns off per-server WHOIS pacing for this
// client. Pacing exists to stop a bulk run hammering one server, so
// leaving it on is right for almost every caller. A single lookup
// whose referral chain happens to hit one server twice -- a registry
// that refers the registrar query back to the same host -- would
// otherwise pay the full interval for the second hop, which is a
// wait no one is served by.
DisableWHOISPacing bool
// HTTPClient is used for RDAP requests only. nil means
// http.DefaultClient. It does NOT cover the IANA bootstrap fetch New
// performs to load RDAP base URLs: that fetch always uses the default
// HTTP client, regardless of this field. A caller behind a proxy who
// needs the bootstrap fetch to go through it as well gets no error --
// New falls back silently to a cached or embedded snapshot instead.
HTTPClient *http.Client
// Resolver supplies RDAP base URLs. nil means load IANA's published
// bootstrap data, which is what almost every caller wants. Set it to
// query a private or mirrored RDAP deployment instead.
//
// Each of NewResolver, NewIPResolver, and NewASNResolver builds a
// Resolver that covers exactly ONE object kind -- there is no
// constructor that combines all three. A Resolver from NewResolver,
// for example, supplies RDAP base URLs for domains only; IP and ASN
// lookups made with it fall back to WHOIS-only, silently, because it
// reports no coverage for those kinds.
Resolver *Resolver
// WHOISIANAServer is the WHOIS server consulted first to discover a
// TLD's registry WHOIS server. Empty means whois.iana.org. Set it to
// use an internal mirror.
WHOISIANAServer string
}
Options configures a Client. The zero value is valid and gives the same defaults the plat CLI uses.
Example ¶
ExampleOptions shows how a program doing many lookups would tune a Client: no disk cache, a longer per-lookup budget, and only the two RDAP sources consulted. It builds no Client and performs no I/O, so it is safe to run as part of the test suite.
package main
import (
"fmt"
"time"
"github.com/patramsey/plat"
)
func main() {
_ = plat.Options{
Timeout: 30 * time.Second,
DisableCache: true,
Sources: []plat.SourceID{
plat.SourceRegistryRDAP,
plat.SourceRegistrarRDAP,
},
}
fmt.Println("configured")
}
Output: configured
type RedactionNotice ¶
type RedactionNotice = model.RedactionNotice
RedactionNotice records that a value was withheld, typically for GDPR.
type RegistrarInfo ¶
type RegistrarInfo = model.RegistrarInfo
RegistrarInfo is a domain's registrar identity.
type Resolver ¶
Resolver maps a TLD, IP address, or ASN to the RDAP base URL that serves it. New builds one from IANA's published bootstrap data; supply your own through Options.Resolver to query a private or mirrored RDAP deployment instead.
A Resolver built by one of the three constructors below covers exactly ONE object kind -- there is no constructor that combines all three. Using, say, a NewResolver Resolver for an IP or ASN lookup is not an error: it simply reports no RDAP coverage for that kind, and the lookup falls back to WHOIS-only.
func NewASNResolver ¶
NewASNResolver builds a Resolver from an explicit ASN-range-to- RDAP-base-URL map, for ASN lookups ONLY. Each key is an inclusive [start, end] autonomous-system number range. Domain and IP lookups made with the result fall back to WHOIS-only, silently -- use NewResolver or NewIPResolver (or both, alongside this) to cover those kinds too.
func NewIPResolver ¶
NewIPResolver builds a Resolver from an explicit prefix-to-RDAP-base-URL map, for IP lookups ONLY. Domain and ASN lookups made with the result fall back to WHOIS-only, silently -- use NewResolver or NewASNResolver (or both, alongside this) to cover those kinds too.
func NewResolver ¶
NewResolver builds a Resolver from an explicit TLD-to-RDAP-base-URL map, for domain lookups ONLY. IP and ASN lookups made with the result fall back to WHOIS-only, silently -- use NewIPResolver or NewASNResolver (or both, alongside this) to cover those kinds too.
type Result ¶
type Result struct {
// Kind says which record pointer below is populated.
Kind Kind
// Input is the caller's original string, unmodified.
Input string
// Domain is set when Kind is KindDomain.
Domain *Record
// IP is set when Kind is KindIP.
IP *IPRecord
// ASN is set when Kind is KindASN.
ASN *ASNRecord
}
Result is one lookup's outcome. Exactly one of Domain, IP, and ASN is non-nil, matching Kind. The three stay separate types because the objects genuinely differ: an IP allocation has no registrar, nameservers, or expiry, and a domain has no address range.
type SourceResult ¶
type SourceResult = model.SourceResult
SourceResult is the per-source outcome behind a record.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
plat
command
|
|
|
internal
|
|
|
diff
Package diff compares two plat snapshots field by field.
|
Package diff compares two plat snapshots field by field. |
|
render
Package render selects which output format cmd/plat uses for a lookup and detects whether stdout is an interactive terminal.
|
Package render selects which output format cmd/plat uses for a lookup and detects whether stdout is an interactive terminal. |
|
render/human
Package human renders a merged domain record as a styled, colorized view for interactive terminals — the FormatHuman counterpart to internal/render/plain's unstyled FormatPlain.
|
Package human renders a merged domain record as a styled, colorized view for interactive terminals — the FormatHuman counterpart to internal/render/plain's unstyled FormatPlain. |
|
render/machine
Package machine encodes a model.Record as the stable JSON/NDJSON wire format described in docs/schema.md.
|
Package machine encodes a model.Record as the stable JSON/NDJSON wire format described in docs/schema.md. |
|
spinner
Package spinner shows an animated progress indicator on an io.Writer (intended to be stderr) while a caller-supplied function runs, then clears it.
|
Package spinner shows an animated progress indicator on an io.Writer (intended to be stderr) while a caller-supplied function runs, then clears it. |
