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 fails if Options.Sources names an unrecognized SourceID,
// or -- rare, since a failed bootstrap fetch falls back to a
// cached copy and then to a snapshot embedded in the binary --
// the bootstrap load itself fails outright.
}
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.
A cancelled or expired context is handled the same way but is not treated as a lookup failure: Lookup returns ctx's own error -- context.Canceled or context.DeadlineExceeded, matched with errors.Is -- rather than ErrLookupFailed, alongside a Result holding whatever had already merged before the context ended.
plat for behavior, model for data ¶
The data types -- Record, IPRecord, ASNRecord, Field[T], and the rest -- are defined in the public package model, and aliased here so that a caller who only wants to call New and Lookup rarely needs a second import. Following an alias (e.g. Record) leads straight to model's own documentation, and because an alias is the same type, not a copy, there is no conversion at the boundary: a model.Record returned by some other package is a plat.Record and vice versa.
That directness cuts both ways: these shapes are public API. Adding a field is additive and safe; renaming or removing an exported field, or changing an aliased method's signature, is a breaking change for every consumer, exactly as if it were declared in this package directly.
Producing plat's JSON output ¶
EncodeJSON writes a Result as plat's documented "schemaVersion": 1 JSON document -- byte-identical to what the CLI's -o json prints for the same record, including with EncodeOptions.Raw for embedded source payloads. EncodeNDJSON writes the same document as a single newline-delimited record; for one Result the two encoders produce identical bytes; NDJSON's value is streaming many Results into one stream, the way the CLI's -o ndjson does across multiple names.
Stability ¶
This API is v0 and may change before 1.0.
Index ¶
- Constants
- Variables
- func EncodeJSON(w io.Writer, res Result, opts EncodeOptions) error
- func EncodeNDJSON(w io.Writer, res Result, opts EncodeOptions) error
- type ASNRecord
- type Client
- type Conflict
- type EncodeOptions
- type Field
- type IPRecord
- type Kind
- type LifecycleInfo
- type LifecycleStage
- type Options
- type OrgInfo
- type Record
- type RedactionNotice
- type RegistrarInfo
- type Resolver
- type ResolverConfig
- type Result
- type SourceID
- type SourceResult
- type TimeValue
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.
const SchemaVersion = machine.SchemaVersion
SchemaVersion is the version of the JSON document EncodeJSON and EncodeNDJSON emit -- currently 1. It is the same schema the plat CLI's -o json produces, and the same number that appears in the output's "schemaVersion" field. A breaking change to the document's shape bumps it. Defined as an alias of machine.SchemaVersion, an internal package's constant, so the two can never drift out of sync.
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") )
var ErrNoRecord = errors.New("plat: Result carries no record")
ErrNoRecord reports an attempt to encode a Result that carries no record. Lookup always populates one -- even when every source fails -- so this can only arise from a hand-built Result, and it is an error rather than a silent "null" so the mistake surfaces where it is made.
Functions ¶
func EncodeJSON ¶ added in v0.5.0
func EncodeJSON(w io.Writer, res Result, opts EncodeOptions) error
EncodeJSON writes res as a schemaVersion 1 JSON document, the same bytes the plat CLI's -o json emits for the same record. A caller never chooses between per-object-type encoders: EncodeJSON dispatches on whichever of res.Domain, res.IP, or res.ASN is non-nil (not on res.Kind), which is what makes ErrNoRecord -- rather than a nil-pointer panic -- the outcome of a hand-built Result whose pointer disagrees with its Kind.
Example ¶
ExampleEncodeJSON builds a Record by hand -- no lookup, no network -- and encodes it the same way the CLI's -o json would. Its output is deterministic, so it runs as part of the test suite.
package main
import (
"bytes"
"fmt"
"github.com/patramsey/plat"
)
func main() {
rec := plat.Record{
Domain: plat.Field[string]{
Value: "example.com",
Sources: []plat.SourceID{plat.SourceRegistryRDAP},
},
}
res := plat.Result{Kind: plat.KindDomain, Input: "example.com", Domain: &rec}
var buf bytes.Buffer
if err := plat.EncodeJSON(&buf, res, plat.EncodeOptions{}); err != nil {
panic(err)
}
fmt.Println(buf.String())
}
Output: {"schemaVersion":1,"objectType":"domain","domain":{"value":"example.com","sources":["registry-rdap"]},"conflicts":[],"redacted":[],"sources":[]}
func EncodeNDJSON ¶ added in v0.5.0
func EncodeNDJSON(w io.Writer, res Result, opts EncodeOptions) error
EncodeNDJSON writes res as a single newline-delimited JSON record, the form the CLI's -o ndjson emits, for streaming many results into one stream. It dispatches on res.Domain/IP/ASN exactly as EncodeJSON does; see that doc comment for how ErrNoRecord arises.
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.
New returns an error in two cases: Options.Sources names a SourceID New does not recognize, or the bootstrap load fails outright. The second is rare in practice -- 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 -- but the first is a validation check on every call, not a corner case.
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. A cancelled or expired ctx is none of those: Lookup returns ctx's own error unwrapped, so errors.Is matches context.Canceled or context.DeadlineExceeded and deliberately does not match ErrLookupFailed.
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 EncodeOptions ¶ added in v0.5.0
type EncodeOptions struct {
// Raw includes each source's unparsed response payload, under
// sources[].raw. It is off by default because the payloads are large
// and most consumers want the merged record, not the wire data.
Raw bool
}
EncodeOptions controls what the encoders include.
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. An
// unrecognized SourceID is rejected by New, not silently ignored --
// see New's doc comment.
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 and the IANA bootstrap fetch that
// New performs to load RDAP base URLs. nil means http.DefaultClient.
// Exposed so an embedding library can route plat's every outbound request
// through a caller-supplied transport, for a proxy or for instrumentation.
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.
//
// Build one with NewResolver, passing a ResolverConfig. Any field of
// ResolverConfig left nil means plat has no RDAP endpoint for that
// object kind, and lookups of it fall back to WHOIS-only, silently --
// that is not an error, just reduced coverage.
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.
Build one with NewResolver, passing a ResolverConfig. A field left nil means plat has no RDAP endpoint for that object kind, and lookups of it fall back to WHOIS-only -- that is not an error, just reduced coverage.
func NewResolver ¶
func NewResolver(cfg ResolverConfig) *Resolver
NewResolver builds a Resolver from an explicit set of RDAP endpoints, for querying a private or mirrored RDAP deployment instead of the ones IANA publishes. Pass it as Options.Resolver.
type ResolverConfig ¶ added in v0.5.0
type ResolverConfig struct {
// Domains maps a TLD (no leading dot, e.g. "com") to its RDAP base URL.
Domains map[string]string
// Prefixes maps an IP prefix to the RDAP base URL serving it. The
// most specific matching prefix wins.
Prefixes map[netip.Prefix]string
// ASNs maps an inclusive [start, end] autonomous-system number range
// to its RDAP base URL.
ASNs map[[2]uint32]string
}
ResolverConfig describes which RDAP base URLs a Resolver should serve. Any field may be nil, meaning plat has no RDAP endpoint for that object kind and lookups of it fall back to WHOIS-only.
One config covers all three kinds deliberately. The predecessor API had a separate constructor per kind, which made it easy to point plat at a private RDAP deployment for domains and, without noticing, lose RDAP for every IP and ASN lookup.
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. |
|
source
Package source holds the pre-merge shapes internal/collect produces and internal/merge consumes, plus the two normalisation helpers collect needs.
|
Package source holds the pre-merge shapes internal/collect produces and internal/merge consumes, plus the two normalisation helpers collect needs. |
|
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. |
|
Package model defines plat's data model: Record, IPRecord, and ASNRecord, the provenance-carrying Field[T] that composes them, and the supporting types -- Conflict, RedactionNotice, SourceResult, LifecycleInfo -- that describe how a merged value came to be what it is.
|
Package model defines plat's data model: Record, IPRecord, and ASNRecord, the provenance-carrying Field[T] that composes them, and the supporting types -- Conflict, RedactionNotice, SourceResult, LifecycleInfo -- that describe how a merged value came to be what it is. |
