plat

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 14 Imported by: 0

README

plat

Domain lookups, reconciled — RDAP and WHOIS, together

Beta CI codecov Latest Release Go Version

Look up a domain's registration record — RDAP and WHOIS, queried concurrently from both registry and registrar, merged into one record with per-field source provenance: which source supplied each value, and where sources disagree.


demo

(Recorded with vhs — see docs/demo.tape for the source script.)

Contents

Why not just whois or an RDAP client?

Domain registration data lives in two parallel, incompatible worlds. WHOIS (RFC 3912) is plaintext over port 43 — no schema, wildly inconsistent formats per registry/registrar, and a referral chain (IANA → registry → registrar) you have to follow by hand. RDAP (RFC 7480–7484, 9082, 9083) is structured JSON over HTTPS, but coverage is incomplete (plenty of ccTLDs still don't run it), registrar RDAP quality varies widely, and it redacts contact data differently than WHOIS does for the same domain.

No single source is complete. Registry and registrar data genuinely differ (thin vs. thick), and RDAP/WHOIS for the same domain often disagree or redact different fields outright. The usual workaround: run whois, squint at unparseable text, try an RDAP client, diff the two mentally.

plat queries all four sources — registry RDAP, registrar RDAP, registry WHOIS, registrar WHOIS — concurrently, merges them into one record, and shows exactly where every field came from and where sources disagree, instead of making you do that reconciliation by hand.

Install

Homebrew (macOS / Linux):

brew install patramsey/tap/plat

Download a release binary:

# macOS (Apple Silicon)
curl -L https://github.com/patramsey/plat/releases/latest/download/plat_darwin_arm64.tar.gz | tar xz
sudo mv plat /usr/local/bin/

# macOS (Intel)
curl -L https://github.com/patramsey/plat/releases/latest/download/plat_darwin_amd64.tar.gz | tar xz
sudo mv plat /usr/local/bin/

# Linux (amd64)
curl -L https://github.com/patramsey/plat/releases/latest/download/plat_linux_amd64.tar.gz | tar xz
sudo mv plat /usr/local/bin/

All platforms and checksums on the releases page.

Go install:

go install github.com/patramsey/plat/cmd/plat@latest

This builds from source without goreleaser's version stamping — plat version will show dev instead of a real version/commit/date, since that's only injected via -ldflags at release build time.

Usage

# Basic lookup — auto-detects a styled human view on a terminal, plain
# text when piped
plat example.com

# Multiple domains in one invocation
plat example.com example.org

# Bulk mode: read names from a file, one per line -- blank lines and
# # comments are skipped -- or from stdin with -
plat --file names.txt
cat names.txt | plat --file -

# Names are looked up concurrently (--concurrency, default 4; also
# applies to names given on the command line), but results are always
# emitted in input order regardless of which lookup finishes first, so
# two runs of the same list produce identical output. WHOIS queries are
# paced per server -- including referral hops to registrar servers -- so
# a large single-TLD list cannot hammer one server. That per-server pace
# (1 query/second) is a floor on wall time no --timeout can raise: a
# 300-name single-TLD list takes at least 300s regardless of --timeout,
# while a mixed-TLD list of the same size finishes faster since the
# floor applies per server, not per run. A long bulk run also
# prints a "looking up... N/total" progress counter to stderr when stderr
# is a terminal and output is the default human format (suppressed for
# piped/redirected output and for -o plain/json/ndjson).
plat --file names.txt --concurrency 8 -o ndjson > results.ndjson

# IP-address lookup — the netblock and its holding organization, from
# the RIR's RDAP + WHOIS, merged the same way (see "IP Lookups" below)
plat 8.8.8.8
plat 2001:4860:4860::8888

# ASN lookup — the autonomous system and its holding organization (see
# "ASN Lookups" below)
plat AS15169

# Machine-readable output
plat example.com -o json | jq .expires.value
plat example.com example.org -o ndjson

# Include raw source payloads alongside the merged record
plat example.com -o json --raw

# Compare a fresh lookup against a saved -o json snapshot -- reports
# what changed (expiry, nameservers, status, ...) and exits 4 if
# anything did. Works for domains, IPs, and ASNs.
plat example.com -o json > before.json
plat --diff before.json example.com

# --diff compares merged values only, not provenance, so a source that
# flaps between runs (a rate-limited RIR, a timed-out registrar WHOIS)
# does not itself trigger exit 4 as long as the remaining sources still
# agree on each field's underlying value. It can still under- or
# over-report a change if that flap removes a field's only supplying
# source, or flips a value's serialized precision/format without
# changing the underlying fact -- see internal/diff's package doc
# comment for the exact edge cases.

# Restrict which sources are queried
plat example.com --source rdap       # registry + registrar RDAP only
plat example.com --source whois      # registry + registrar WHOIS only
plat example.com --source registry   # registry RDAP + registry WHOIS only
plat example.com --source registrar  # registrar RDAP + registrar WHOIS only

# Skip the registrar RDAP related-link hop
plat example.com --no-follow

# One-line summary per domain (lock status, expiry, conflict count)
# instead of the full view -- ignored for -o json/ndjson
plat example.com -q

# Adjust the per-source timeout (default 5s). It bounds time spent
# talking to a server: in a bulk run, the time a name spends waiting its
# turn for a paced WHOIS server is not charged against it.
plat example.com --timeout 10s

# Show the per-source diagnostic block: which sources were attempted,
# their latency, and their status. Also surfaces this detail when a
# lookup fails outright, not just on success.
plat example.com -v

# Show the full per-source breakdown for every conflicted field. Without
# this, a conflicted field is still marked (⚠ in the human view,
# [conflict] in plain — never silently hidden) — this flag just reveals
# what each source actually reported.
plat example.com --conflicts

# Force a fresh fetch of the IANA RDAP bootstrap file, bypassing the
# cached copy
plat example.com --refresh-bootstrap

# Disable color output -- same effect as setting NO_COLOR (any
# non-empty value: https://no-color.org/), which plat also honors
plat example.com --no-color
NO_COLOR=1 plat example.com

# Version and shell completions
plat version
plat --version                # equivalent shortcut
plat version -o json          # machine-readable
plat version --full           # include Go version and platform
plat completion bash > /etc/bash_completion.d/plat
plat completion zsh > "${fpath[1]}/_plat"
plat completion fish > "$(dirname "$(command -v fish)")/../share/fish/vendor_completions.d/plat.fish"
plat completion powershell > plat.ps1

IP Lookups

plat also looks up IP addresses: plat 8.8.8.8 or plat 2001:4860:4860::8888 finds the RIR (ARIN, RIPE NCC, APNIC, LACNIC, or AFRINIC) that holds the containing netblock and queries its RDAP and WHOIS, merged with the same per-field provenance as a domain lookup.

There's no registrar leg — an IP allocation has no registrar — so only registry-rdap/registry-whois ever appear as sources, and the fields are a netblock's own (handle, CIDR, start/end address, parent handle, holding organization) rather than a domain's (registrar, nameservers, expiry, DNSSEC). -o json sets "objectType": "ip" to distinguish the shape from a domain record's "objectType": "domain"; see docs/schema.md for the full field reference. Reserved/private addresses (10.0.0.1, 127.0.0.1, ::1, ...) are rejected up front with a usage error, since no RIR allocates them to an organization.

ASN Lookups

plat also looks up autonomous system numbers: plat AS15169 finds the RIR that holds the ASN and queries its RDAP and WHOIS, merged with the same per-field provenance as a domain or IP lookup. The AS prefix is required (case-insensitive) — a bare number like plat 15169 is treated as a (invalid, single-label) domain rather than an ASN, since it's likelier a typo than an intentional ASN lookup.

Like an IP lookup, there's no registrar leg, so only registry-rdap/registry-whois ever appear as sources. The fields are an autonomous system's own (handle, AS name, start/end autnum range, holding organization) rather than a domain's or netblock's. -o json sets "objectType": "asn" to distinguish the shape from a domain or IP record's; see docs/schema.md for the full field reference.

Output & Provenance

graph LR
    RR[RR registrar-rdap]
    GR[GR registry-rdap]
    RW[RW registrar-whois]
    GW[GW registry-whois]

    RR --> Merge
    GR --> Merge
    RW --> Merge
    GW --> Merge

    Merge{{"merge engine<br/>precedence: RR &gt; GR &gt; RW &gt; GW"}}

    Merge -->|sources agree| Field["field value +<br/>agreeing source codes"]
    Merge -->|sources disagree| Conflict["⚠ Conflict<br/>hidden by default, --conflicts reveals"]
    Merge -->|higher-precedence value redacted| Redacted["Redacted notice<br/>next-highest populated value wins instead"]

(Expires is the one exception to strict precedence: on a genuine conflict, it picks the earliest disputed date instead — see below.)

Source codes

Every field carries the sources that agreed on its value, shown as a 2-letter code — RR registrar-rdap, GR registry-rdap, RW registrar-whois, GW registry-whois — rather than full names, since that badge repeats on every field and full names ("registrar-rdap, registry-rdap, registry-whois") added up to real visual noise on a well-agreed-upon record. A one-line legend decoding the codes prints once per lookup in the human/plain views.

-o json/-o ndjson keep full source names in sources[] — machine consumers don't need the abbreviation, and it isn't part of the stable schema. String comparisons are normalized (case, whitespace, a trailing period) before sources are judged to agree or disagree, so formatting-only differences never show up as noise.

Conflicts

Where sources genuinely disagree, the conflict is recorded, not silently dropped:

  • The field is marked — in the human view, [conflict] in plain — never invisible either way.
  • The top summary shows a running conflict count.
  • --conflicts reveals every disagreeing source's exact value (off by default, so a domain with several noisy timestamp/nameserver disagreements doesn't dominate the view).
  • -o json's conflicts[] array always includes the full detail regardless of the flag — machine output has no "too much detail" problem.

Expires is the one field where a conflict changes which value wins: it shows the earliest disputed date rather than the usual highest-precedence source, since assuming more runway than you actually have is the riskier mistake for an expiration date. Every other field keeps the highest-precedence value even in conflict.

Redaction and contacts

GDPR-style redaction is modeled explicitly, not mistaken for a literal contact name — the same handling covers the Registrar Name field itself, when a registrar's own identity comes back redacted.

Registrant/admin/tech/billing contact details are deliberately not shown, for two reasons:

  • Since ICANN's 2018 GDPR Temporary Specification, the large majority of that data comes back redacted from every source anyway — building out full contact parsing would mostly render "REDACTED FOR PRIVACY" over and over, not real ownership data.
  • RDAP represents contacts as jCard (RFC 7095) — a deliberately unpleasant format to parse defensively — and WHOIS's own contact-block conventions are even less consistent than its other fields.

plat spends its effort on the fields that are reliably available and comparable across all four sources — registrar identity, dates, nameservers, status, abuse contact — where per-field provenance actually earns its keep. (Registrar Abuse Email/Phone are shown: they're the registrar's own operational contact, not a registrant's personal data, and aren't typically redacted.)

Lifecycle

For a gTLD domain that's expired, plat interprets its EPP status into a plain-language lifecycle stage — Auto-Renew Grace Period, Redemption Grace Period, Pending Restore, or Pending Delete — with an estimated (never confirmed) end date wherever ICANN's Expired Registration Recovery Policy (ERRP) or a common registry convention gives one a fixed or capped duration to derive from. Only Redemption Grace's 30 days is actually ICANN-mandated (ERRP §3.1); Auto-Renew Grace's 45 days is a registry convention (e.g. Verisign's for .com/.net), and ERRP explicitly leaves registrars free to act sooner at their own discretion. It's shown as its own section in the human/plain views and as a lifecycle object in JSON (see docs/schema.md); ccTLDs, internationalized (IDN) TLDs, and domains without a recognized lifecycle-relevant status don't get one.

Human view vs. JSON

The styled human view (default on a real terminal, or forced with -o human) leads with an at-a-glance summary — lock status, expiry countdown, conflict count — inside a bordered box color-coded to match: a domain locked down with transfer/update/delete protections gets a calm green border, one with something actively wrong (held, pending delete) gets red. EPP status codes are color-coded the same way. The Registrar URL is a clickable OSC 8 hyperlink in terminals that support it, and degrades to plain text everywhere else (including pipes, where all styling and the hyperlink are stripped automatically).

The -o json/-o ndjson wire format is a versioned, stable schema, unaffected by -v or any of the styling above — see docs/schema.md for the full field-by-field reference. --diff -o json emits a different, separately-versioned schema (a report of what changed, not a record) and doesn't emit the record schema at all — see docs/schema.md's --diff output section.

Exit Codes

Code Meaning
0 At least one source returned usable data (and, with --diff, nothing changed)
1 Every attempted source agrees the domain doesn't exist
2 Usage error (bad flag, invalid domain input)
3 Total lookup failure (no source reachable, or ambiguous failure state)
4 --diff found at least one changed field

For multiple domains in one invocation, the overall exit code is the worst of every individual domain's code.

Exit 4 only fires once the lookup itself has actually succeeded — a not-found or a total-failure result still exits 1 or 3, so a monitoring script checking $? never mistakes "the domain vanished" for "something changed."

Exit 1 is checking-availability's "good" outcome, not an error — and it reads that way in the human view too. plat unregistered-example.com prints is not registered (checked: registry-rdap) in the same calm color as a locked status or a successful DNSSEC check, not the red used for exit 3.

Exit 3's message distinguishes two different failure shapes:

  • a total connectivity failure: lookup failed — no sources could be reached
  • a mixed result where non-existence can't be confirmed: lookup inconclusive — N of M sources failed

Use as a Go library

The lookup engine behind the CLI is importable directly, with no need to shell out to the plat binary:

go get github.com/patramsey/plat
c, err := plat.New(ctx, plat.Options{})
if err != nil {
	log.Fatal(err)
}

res, err := c.Lookup(ctx, "example.com")
if err != nil {
	log.Fatal(err)
}

fmt.Println(res.Domain.Expires.Value.Time, res.Domain.Expires.Sources)

Build one Client and reuse it for every Lookup — it holds the IANA bootstrap data and a per-server WHOIS pacing limiter, both of which are only useful if kept around. Every field on the result types is a Field[T] carrying both the merged value and which sources supplied it, same as the CLI's output; a source failing is normal, not an error, as long as at least one source returns data.

Record, IPRecord, ASNRecord, Field[T], and the other data types are defined in github.com/patramsey/plat/model and aliased into plat, so plat.Record and model.Record are the same type — import model directly only if you want its documentation without also pulling in Client and Lookup.

EncodeJSON writes a Result as plat's schemaVersion: 1 JSON — byte-identical to what -o json prints for the same lookup:

_ = plat.EncodeJSON(os.Stdout, res, plat.EncodeOptions{})

EncodeNDJSON writes the same document as a single newline-delimited record; for one Result it produces identical bytes to EncodeJSON — its purpose is streaming many Results into one stream, mirroring the CLI's -o ndjson.

This API is v0 and may change before 1.0. See go doc github.com/patramsey/plat for the full reference.

License

MIT

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

Examples

Constants

View Source
const (
	SourceRegistrarRDAP  = model.SourceRegistrarRDAP
	SourceRegistryRDAP   = model.SourceRegistryRDAP
	SourceRegistrarWHOIS = model.SourceRegistrarWHOIS
	SourceRegistryWHOIS  = model.SourceRegistryWHOIS
)

The four sources plat merges, in precedence order.

View Source
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.

View Source
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

View Source
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")
)
View Source
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 ASNRecord

type ASNRecord = model.ASNRecord

ASNRecord is a merged, provenance-annotated autonomous system result.

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

func New(ctx context.Context, opts Options) (*Client, error)

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

func (c *Client) Lookup(ctx context.Context, input string) (Result, error)

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)
	}
}

type Conflict

type Conflict = model.Conflict

Conflict records a field where sources disagreed.

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

type Field[T any] = model.Field[T]

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 IPRecord

type IPRecord = model.IPRecord

IPRecord is a merged, provenance-annotated IP network lookup result.

type Kind

type Kind int

Kind is the sort of object a Result describes.

const (
	// KindDomain is a domain name.
	KindDomain Kind = iota
	// KindIP is an IP address allocation. IPv4 and IPv6 are not
	// distinguished here; IPRecord.IPVersion carries that.
	KindIP
	// KindASN is an autonomous system.
	KindASN
)

func (Kind) String

func (k Kind) String() string

String returns the kind's name, as used in -o json's objectType.

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 OrgInfo

type OrgInfo = model.OrgInfo

OrgInfo is the organization holding an IP allocation or ASN.

type Record

type Record = model.Record

Record is a merged, provenance-annotated domain lookup result.

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

type Resolver = bootstrap.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 SourceID

type SourceID = model.SourceID

SourceID names one of the four sources plat can consult.

type SourceResult

type SourceResult = model.SourceResult

SourceResult is the per-source outcome behind a record.

type TimeValue

type TimeValue = model.TimeValue

TimeValue is a timestamp with the raw string it was parsed from.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL