Documentation
¶
Overview ¶
Package whois looks up domain registration data and reports whether a domain is free to register.
It ships a table of around 880 TLDs, each mapped to the service that answers for it, and queries that service with the WHOIS protocol on port 43 or over HTTP for the registries that only serve RDAP or a web form. Registries share no wording for "no such domain", so the answer is classified by a detector built from the wording of each one: see PatternDetector.Detect.
Getting started ¶
The package-level functions use a shared client with the bundled table:
result, err := whois.Lookup(ctx, "monovm.com")
if err != nil {
return err
}
fmt.Println(result.Status) // available, unavailable or premium
A batch check takes several names at once, queries them concurrently, and expands a name given without a TLD over the popular ones:
for domain, status := range whois.Check(ctx, "monovm.com", "example") {
fmt.Println(domain, status)
}
Names in other scripts are converted to the ASCII form a lookup requires, so "münchen.de" is queried as "xn--mnchen-3ya.de".
Errors are not verdicts ¶
A registry that is busy, rate-limiting or silent tells you nothing about a name. Every such case is an error here rather than a status, because reading one as "registered" or "available" is how an availability check starts lying. See the error list in errors.go.
Composition ¶
A lookup is made of three parts, each an interface with a default this package provides and an option to replace it:
- Registry resolves a TLD to the whois service for it. Definitions is the bundled implementation; WithRegistry takes another.
- Transport carries the query. SocketTransport speaks WHOIS, HTTPTransport speaks HTTP and RDAP, and Router picks between them; WithTransport takes another, which is what a test or a caching layer supplies.
- Detector reads the answer. PatternDetector classifies by registry wording from Rules; WithDetector takes another.
Build a Client with NewClient to set any of them, along with the timeouts, concurrency and popular TLDs.
Source layout ¶
The files follow those three parts, and within each part the decision, the data it decides on, and the text it is read from live apart:
whois.go the package-level functions, over a shared client
client.go Client: resolve, query, classify, and stop rather than guess
options.go Option: one per decision a caller can make
config.go the defaults, and how a Client is assembled from them
domain.go Domain: validation, IDNA, splitting a name from its TLD
registry.go the Registry interface and the bundled TLD table
definition.go one TLD definition, and reading a definition file
transport.go the Transport interface, Router and the response type
transport_socket.go, transport_http.go
the two transports: WHOIS on port 43, and HTTP or RDAP
detector.go the Detector interface and PatternDetector: the orchestration
checks.go the classification as an ordered list of steps
scan.go the response under inspection, and the questions asked of it
rules.go Rules: the tables, and the matching each table knows
rules_data.go the wording that applies to every registry
rules_tld.go the wording of individual registries
rdap.go reading a verdict out of an RDAP document
text.go reducing a response to the lines that carry meaning
result.go Status, Result and CheckResult
details.go Details: which check reached the verdict
errors.go the errors, none of which is a verdict
data/ the embedded TLD table, dist.whois.json
cmd/whois/ the command line tool
This package is a port of github.com/monovm/whois-php and reaches the same verdicts, aside from the non-answers described above.
Index ¶
- Constants
- Variables
- func CanLookup(tld string) bool
- func Check(ctx context.Context, domains ...string) map[string]Status
- func DetectAvailability(response, tld string, registryMarkerMatched bool) (bool, error)
- func SplitDomain(domain string) (sld, tld string)
- type CheckResult
- type Client
- func (c *Client) CanLookup(tld string) bool
- func (c *Client) Check(ctx context.Context, domains ...string) map[string]Status
- func (c *Client) CheckAll(ctx context.Context, domains ...string) []CheckResult
- func (c *Client) Lookup(ctx context.Context, domain string) (*Result, error)
- func (c *Client) LookupDomain(ctx context.Context, domain Domain) (*Result, error)
- func (c *Client) PopularTLDs() []string
- func (c *Client) Registry() Registry
- type Definition
- type Definitions
- type Details
- type Detector
- type Domain
- type HTTPTransport
- type Option
- func WithConcurrency(n int) Option
- func WithDefinitionOverrides(data []byte) Option
- func WithDefinitions(defs Definitions) Option
- func WithDetector(detector Detector) Option
- func WithDialer(dialer *net.Dialer) Option
- func WithHTTPClient(client *http.Client) Option
- func WithHTTPTimeout(timeout time.Duration) Option
- func WithInsecureTLS(insecure bool) Option
- func WithMaxResponseSize(n int64) Option
- func WithPopularTLDs(tlds ...string) Option
- func WithRegistry(registry Registry) Option
- func WithTimeout(timeout time.Duration) Option
- func WithTransport(transport Transport) Option
- func WithUserAgent(userAgent string) Option
- type Outcome
- type PatternDetector
- type Patterns
- type Phrases
- type Registry
- type Response
- type Result
- type Router
- type Rules
- type SocketTransport
- type Status
- type TLDPatterns
- type Transport
Examples ¶
Constants ¶
const ( // DefaultTimeout bounds one query over the WHOIS protocol, connect and read // together. DefaultTimeout = 10 * time.Second // DefaultHTTPTimeout bounds one query against an HTTP or RDAP endpoint, which // are slower than a WHOIS server and often redirect first. DefaultHTTPTimeout = 60 * time.Second // DefaultConcurrency is how many names a batch queries at once. DefaultConcurrency = 8 // DefaultMaxResponseSize caps how much of an answer is read. DefaultMaxResponseSize = 1 << 20 // 1 MiB // DefaultUserAgent identifies this package to an HTTP endpoint. DefaultUserAgent = "whois-go (+https://github.com/monovm/whois-go)" )
Defaults a client falls back on when the matching option is not given.
Variables ¶
var ( // ErrInvalidDomain is returned for a name that cannot be queried: empty, // without a TLD, too long, or holding a character a domain name cannot hold. ErrInvalidDomain = errors.New("whois: invalid domain name") // ErrNoWhoisServer is returned when no definition covers the TLD, or when // the definition carries no endpoint. ErrNoWhoisServer = errors.New("whois: no whois server known for tld") // ErrTLDNotSupported is returned when the server answered that it does not // serve this TLD. ErrTLDNotSupported = errors.New("whois: tld is not supported by the whois server") // rate-limit or timeout notice, or with a status that means the same. The // lookup may succeed if it is retried later. ErrServerUnavailable = errors.New("whois: whois server is temporarily unavailable or rate-limited") // ErrEmptyResponse is returned when the answer is blank, which says nothing // about the domain. ErrEmptyResponse = errors.New("whois: empty response from whois server") // ErrResponseTooLarge is returned when the answer passes the configured // size cap. The part that arrived is discarded rather than judged, since a // verdict from half an answer cannot be trusted. ErrResponseTooLarge = errors.New("whois: response larger than the configured limit") )
The errors a lookup can report. Each is wrapped with the domain, endpoint or reason it applies to, so match them with errors.Is rather than by comparison.
Only ErrInvalidDomain and ErrNoWhoisServer say something about the name itself. The others mean the lookup produced no verdict, and none of them may be read as "the domain is registered": a busy server, a truncated answer and silence all look alike from the outside, and treating them as an answer is how an availability check quietly starts lying.
Functions ¶
func CanLookup ¶
CanLookup reports whether the bundled table knows a whois service for the TLD, which may be given with or without a leading dot.
func Check ¶
Check looks up one or more names with the default client and reports a status per name. See Client.Check.
Example ¶
package main
import (
"context"
"fmt"
whois "github.com/monovm/whois-go"
)
func main() {
ctx := context.Background()
// "example" carries no TLD, so it is expanded over the popular ones.
for domain, status := range whois.Check(ctx, "monovm.com", "example") {
fmt.Println(domain, status)
}
}
Output:
func DetectAvailability ¶
DetectAvailability reports whether a whois or RDAP response says the domain is unregistered, using the default detector. Pass the response as the server sent it. See PatternDetector.Detect for the errors and the order of the checks.
Example ¶
DetectAvailability classifies an answer obtained elsewhere, without querying anything.
package main
import (
"fmt"
"log"
whois "github.com/monovm/whois-go"
)
func main() {
response := `No match for "THISDOMAINDOESNOTEXIST12345.COM".`
available, err := whois.DetectAvailability(response, ".com", false)
if err != nil {
log.Fatal(err)
}
fmt.Println(available)
}
Output: true
Example (NoVerdict) ¶
A registry that will not answer produces an error rather than a verdict.
package main
import (
"errors"
"fmt"
whois "github.com/monovm/whois-go"
)
func main() {
available, err := whois.DetectAvailability("Number of allowed queries exceeded.", ".us", false)
fmt.Println(available)
fmt.Println(errors.Is(err, whois.ErrServerUnavailable))
}
Output: false true
func SplitDomain ¶
SplitDomain splits a domain into its second-level and top-level parts at the first dot, without validating either: "monovm.co.uk" yields "monovm" and ".co.uk", and a name with no dot yields an empty TLD. The result is lower-cased and stripped of surrounding space and any trailing dot.
Use it to ask whether an input carries a TLD at all, as a batch check does before expanding it. Use ParseDomain to obtain a name that is safe to query.
Types ¶
type CheckResult ¶
type CheckResult struct {
// Domain is the name that was checked, as it was queried. For a name given
// without a TLD this is one of the expanded names, and for an input that could
// not be parsed it is the input itself.
Domain string
// Status is the verdict, or StatusInvalid or StatusError when there is none.
Status Status
// Result is the full result, and nil when Status is StatusInvalid or
// StatusError.
Result *Result
// Err is the failure behind StatusInvalid or StatusError, and nil otherwise.
Err error
}
CheckResult pairs one checked name with its outcome. A batch reports these so that a failure on one name neither hides the others nor is mistaken for a verdict about it.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client looks up domains against the whois service of their TLD.
It composes the three parts a lookup is made of, each replaceable through an option: a Registry that resolves a TLD to its service, a Transport that carries the query, and a Detector that reads the answer. A Client is safe for concurrent use and one per process is enough; build it with NewClient.
func DefaultClient ¶
DefaultClient returns the client the package-level functions use: the bundled TLD table, the standard transports and the default detector.
func NewClient ¶
NewClient builds a client from the bundled TLD table, the two standard transports and the default detector, as adjusted by the options.
func (*Client) CanLookup ¶
CanLookup reports whether a whois service is known for the TLD, which may be given with or without a leading dot.
func (*Client) Check ¶
Check looks up one or more names and reports a status per name. A name given without a TLD is expanded over the popular TLDs, so "monovm" yields an entry for monovm.com, monovm.net and so on. Lookups run concurrently, bounded by the configured concurrency.
Failures are reported as StatusInvalid or StatusError rather than returned. Use CheckAll for the reason behind one, or Lookup for a single name.
func (*Client) CheckAll ¶
func (c *Client) CheckAll(ctx context.Context, domains ...string) []CheckResult
CheckAll is Check with the full result and error of every lookup, in the order the names were expanded.
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
whois "github.com/monovm/whois-go"
)
func main() {
client, err := whois.NewClient(
whois.WithPopularTLDs(".com", ".dev", ".io"),
whois.WithConcurrency(16),
whois.WithTimeout(5*time.Second),
)
if err != nil {
log.Fatal(err)
}
for _, result := range client.CheckAll(context.Background(), "monovm", "example.org") {
if result.Err != nil {
// A failure is not a verdict: the name may well be registered.
fmt.Printf("%s: could not check: %v\n", result.Domain, result.Err)
continue
}
fmt.Printf("%s: %s\n", result.Domain, result.Status)
}
}
Output:
func (*Client) Lookup ¶
Lookup queries the whois service for one domain and reports whether the name is registered. The name must carry a TLD: Lookup does not guess one, unlike Check.
It returns ErrInvalidDomain for a malformed name, ErrNoWhoisServer when no definition covers the TLD, and ErrServerUnavailable, ErrTLDNotSupported or ErrEmptyResponse when the server answered without reporting on the name. None of those may be read as "the name is registered".
func (*Client) LookupDomain ¶
LookupDomain is Lookup for a name that is already parsed, which saves parsing it again in a caller that keeps Domain values around.
It is the one place the three parts meet, and it does nothing itself: the registry resolves, the transport carries, the detector reads, and each step stops the lookup rather than guessing when it cannot go on.
func (*Client) PopularTLDs ¶
PopularTLDs returns the TLDs a name given without one is expanded over.
type Definition ¶
type Definition struct {
// Extensions is the TLD this definition was resolved for, with its leading
// dot. In the definition file one entry covers a comma-separated group of
// TLDs; parsing splits the group into one definition per TLD.
Extensions string `json:"extensions,omitempty"`
// URI is the endpoint to query. "socket://host[:port]" is the WHOIS protocol
// over TCP, port 43 unless another is given; anything else is an HTTP URL
// the domain name is appended to.
URI string `json:"uri"`
// Available is the wording this registry answers with when it holds no
// record for a name. A marker written with a leading "---" is anchored to
// the start of the response; see the scan prefix in detector.go.
Available string `json:"available"`
// Premium is the wording marking a premium or otherwise restricted name.
// Empty for most registries.
Premium string `json:"premium,omitempty"`
// Comment carries free-form notes from the definition file.
Comment string `json:"comment,omitempty"`
}
Definition describes how to query the whois service responsible for a TLD.
func (Definition) Address ¶
func (d Definition) Address() string
Address returns the "host:port" to dial for a socket definition, defaulting to the WHOIS port. It returns an empty string for an HTTP definition.
func (Definition) IsSocket ¶
func (d Definition) IsSocket() bool
IsSocket reports whether the definition is queried with the WHOIS protocol.
type Definitions ¶
type Definitions map[string]Definition
Definitions maps a TLD, leading dot included and lower-cased, to the whois service that answers for it. It implements Registry.
func BundledDefinitions ¶
func BundledDefinitions() (Definitions, error)
BundledDefinitions returns the TLD table embedded in the package. The map is shared between callers and must not be modified; use Clone to derive one.
func ParseDefinitions ¶
func ParseDefinitions(data []byte) (Definitions, error)
ParseDefinitions reads definitions in the format of dist.whois.json: an array of objects whose "extensions" field lists, comma separated, the TLDs the entry serves. Each TLD becomes an entry of its own in the returned map.
func (Definitions) CanLookup ¶
func (d Definitions) CanLookup(tld string) bool
CanLookup reports whether a definition exists for the TLD.
func (Definitions) Clone ¶
func (d Definitions) Clone() Definitions
Clone returns a copy that can be modified independently.
func (Definitions) Definition ¶
func (d Definitions) Definition(tld string) (Definition, bool)
Definition implements Registry.
func (Definitions) Merge ¶
func (d Definitions) Merge(other Definitions)
Merge copies every entry of other into d, replacing the entries for TLDs that are already present. It is how an override file extends the bundled table.
func (Definitions) TLDs ¶
func (d Definitions) TLDs() []string
TLDs returns every TLD the table covers, sorted.
type Details ¶
type Details struct {
// RegistryMarkerMatched is true when the wording from the TLD definition was
// found, which settles the verdict before any other check runs.
RegistryMarkerMatched bool `json:"registry_marker_matched"`
// ContainsUnsupportedTLDMessages is true when the server said it cannot
// answer, whether for this TLD or for the moment.
ContainsUnsupportedTLDMessages bool `json:"contains_unsupported_tld_messages"`
// wording of a registered domain, or a notice withholding a reserved name.
ContainsUnavailabilityIndicators bool `json:"contains_unavailability_indicators"`
// ContainsRegistrationIndicators is true when the response reads like a
// registration record or an RDAP domain document.
ContainsRegistrationIndicators bool `json:"contains_registration_indicators"`
// ContainsAvailabilityKeywords is true when a "no record" wording survived
// the removal of comment lines and boilerplate.
ContainsAvailabilityKeywords bool `json:"contains_availability_keywords"`
// ContainsNoMatchPatterns is true when a "no record" pattern matched anywhere
// in the response.
ContainsNoMatchPatterns bool `json:"contains_no_match_patterns"`
// TLDSpecificPatterns is true when the availability wording of this registry
// matched.
TLDSpecificPatterns bool `json:"tld_specific_patterns"`
// DomainStatusIndicators is true for an explicit "available" status, or for a
// response carrying none of the fields of a registration.
DomainStatusIndicators bool `json:"domain_status_indicators"`
// ResponseTooShort is true for a response too small to hold a record. It is
// reported for information: no verdict is drawn from it, because a short
// answer is as often a preamble or a restriction notice as a free name.
ResponseTooShort bool `json:"response_too_short"`
// Available is the verdict, false whenever Outcome is not OutcomeAvailable.
Available bool `json:"available"`
// Outcome is the conclusion, including the three that are not verdicts.
Outcome Outcome `json:"outcome"`
// ResponseLength is the length of the response in bytes.
ResponseLength int `json:"response_length"`
// ResponsePreview holds the first 200 characters of the response.
ResponsePreview string `json:"response_preview"`
}
Details reports which of a detector's checks fired for a response, in the order they are consulted. It is meant for debugging a verdict that looks wrong and for tuning definitions; branch on the verdict itself, not on these fields.
func AvailabilityDetails ¶
AvailabilityDetails reports which checks fired for a response, using the default detector. See PatternDetector.Details.
Example ¶
AvailabilityDetails shows which checks fired, for debugging a verdict that looks wrong.
package main
import (
"fmt"
whois "github.com/monovm/whois-go"
)
func main() {
response := `Domain: example.de
Status: connect
Changed: 2024-01-01T00:00:00+01:00`
details := whois.AvailabilityDetails(response, ".de", false)
fmt.Println(details.Outcome)
fmt.Println(details.ContainsUnavailabilityIndicators)
}
Output: unavailable true
type Detector ¶
type Detector interface {
// Detect reports whether the response says the domain is free. tld selects
// registry-specific rules and may be empty. registryMarkerMatched is the
// result of the substring test from the TLD definition.
//
// An error means the response carries no verdict, so its boolean is false
// and must not be read as "registered".
Detect(response, tld string, registryMarkerMatched bool) (bool, error)
}
Detector decides whether the answer of a whois service means the domain is unregistered. PatternDetector is the implementation this package bundles; supply another to classify with rules of your own.
A Detector may be called from several goroutines at once.
type Domain ¶
type Domain struct {
// contains filtered or unexported fields
}
Domain is a domain name validated and normalized for a whois lookup: lower case, ASCII, without a trailing dot, and split into the parts a lookup needs.
The zero value is not a usable domain; build one with ParseDomain. Values are immutable, so a Domain can be shared and compared freely.
func ParseDomain ¶
ParseDomain validates a domain name and splits it at the first dot, so "example.co.uk" has the second-level part "example" and the TLD ".co.uk". The whois service is defined per suffix and the definitions cover suffixes of more than one label, which is why the split is not at the last dot.
A name written in another script is converted to the ASCII form a lookup requires, as IDNA2008 defines it: "münchen.de" is queried as "xn--mnchen-3ya.de". Surrounding space and any trailing dot are removed first, so a name copied out of a file or a log parses.
It returns ErrInvalidDomain for a name that is empty, carries no TLD, holds a character a domain name cannot hold, or breaks the length limits. Rejecting those here is what keeps a name from reaching the wire, where a newline would let a caller append a second query to a WHOIS request, and a "#" or a "?" would cut off the path of an HTTP endpoint.
An all-ASCII name is checked against the letter-digit-hyphen rules rather than against the full IDNA2008 rules, so that a name a registry accepts is not refused here for being unusual. A punycode label is the exception: "xn--" says the label means something specific, and one that decodes to nothing is refused.
Example ¶
A name in any script can be looked up: it is converted to the ASCII form the registry expects.
package main
import (
"errors"
"fmt"
"log"
whois "github.com/monovm/whois-go"
)
func main() {
domain, err := whois.ParseDomain("münchen.de")
if err != nil {
log.Fatal(err)
}
fmt.Println(domain)
fmt.Println(domain.SLD(), domain.TLD())
// A name that could alter the request it goes into is refused.
_, err = whois.ParseDomain("example.com\r\nsecond.com")
fmt.Println(errors.Is(err, whois.ErrInvalidDomain))
}
Output: xn--mnchen-3ya.de xn--mnchen-3ya .de true
type HTTPTransport ¶
type HTTPTransport struct {
// Client sends the request. Nil means http.DefaultClient.
Client *http.Client
// MaxResponseSize caps the answer. Zero means DefaultMaxResponseSize.
MaxResponseSize int64
// UserAgent identifies the caller. Empty means the package default.
UserAgent string
}
HTTPTransport queries the registries that answer over HTTP instead of the WHOIS protocol, including those serving only RDAP: the domain name is appended to the endpoint from the definition.
The zero value works, using http.DefaultClient, the default size cap and the package user agent.
func (*HTTPTransport) Query ¶
func (t *HTTPTransport) Query(ctx context.Context, domain string, def Definition) (*Response, error)
Query implements Transport.
type Option ¶
type Option func(*clientConfig) error
Option configures a Client. Options are independent of each other and of the order they are passed in.
func WithConcurrency ¶
WithConcurrency sets how many names a batch queries at once.
func WithDefinitionOverrides ¶
WithDefinitionOverrides merges definitions in the format of dist.whois.json over the table in use, replacing the entries for the TLDs they cover. It is the counterpart of the PHP package's whois.json override file, and may be given more than once, each set merged in turn.
Example ¶
A TLD the bundled table does not cover, or one that moved, is added by merging an override in the format of dist.whois.json.
package main
import (
"fmt"
"log"
whois "github.com/monovm/whois-go"
)
func main() {
overrides := []byte(`[
{"extensions": ".example,.test", "uri": "socket://whois.example.test", "available": "No match"}
]`)
client, err := whois.NewClient(whois.WithDefinitionOverrides(overrides))
if err != nil {
log.Fatal(err)
}
fmt.Println(client.CanLookup(".example"))
fmt.Println(client.CanLookup(".com")) // the bundled table is still there
}
Output: true true
func WithDefinitions ¶
func WithDefinitions(defs Definitions) Option
WithDefinitions replaces the bundled TLD table.
func WithDetector ¶
WithDetector replaces the availability detector.
func WithDialer ¶
WithDialer sets the dialer used for the WHOIS protocol, for instance to pick a source address or a resolver.
func WithHTTPClient ¶
WithHTTPClient sets the client used for HTTP and RDAP endpoints.
func WithHTTPTimeout ¶
WithHTTPTimeout bounds one query against an HTTP or RDAP endpoint, leaving the WHOIS timeout alone.
func WithInsecureTLS ¶
WithInsecureTLS turns off certificate verification for HTTP endpoints. A few registry endpoints still serve expired or mismatched certificates, and the PHP package skips verification for every one of them; this package verifies by default and leaves the trade-off to the caller. Turning it off exposes lookups to interception, so scope it to a client used for those registries only.
It has no effect when WithHTTPClient supplies a client with a transport of its own, which is then responsible for its own TLS configuration.
func WithMaxResponseSize ¶
WithMaxResponseSize caps how much of an answer is read. An answer beyond the cap fails with ErrResponseTooLarge rather than being cut short, because a verdict from half an answer cannot be trusted.
func WithPopularTLDs ¶
WithPopularTLDs sets the TLDs a name given without one is expanded over during a batch check. A leading dot is added where it is missing.
func WithRegistry ¶
WithRegistry replaces the source of TLD definitions, for instance to read them from a database or a file reloaded at runtime. It cannot be combined with WithDefinitions or WithDefinitionOverrides.
func WithTimeout ¶
WithTimeout bounds one query. It applies to both transports; pass zero to leave the bound to the context alone.
func WithTransport ¶
WithTransport replaces both transports with one of your own, for instance to answer from a cache, to route queries through a proxy, or to serve fixtures in a test. It makes the transport-level options below inapplicable.
Example ¶
Any of the three parts of a lookup can be replaced: where the definitions come from, how the query is carried, and how the answer is read.
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
whois "github.com/monovm/whois-go"
)
// cachingTransport answers from a map, and stands in for anything that wraps a
// lookup: a cache, a proxy, a rate limiter, or a fixture in a test.
type cachingTransport struct {
answers map[string]string
inner whois.Transport
}
func (c *cachingTransport) Query(ctx context.Context, domain string, def whois.Definition) (*whois.Response, error) {
if body, ok := c.answers[domain]; ok {
return &whois.Response{Body: body, Server: "cache"}, nil
}
return c.inner.Query(ctx, domain, def)
}
func main() {
transport := &cachingTransport{
answers: map[string]string{"cached.com": `No match for "CACHED.COM".`},
inner: &whois.SocketTransport{Timeout: 10 * time.Second},
}
client, err := whois.NewClient(
whois.WithTransport(transport),
whois.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)
if err != nil {
log.Fatal(err)
}
result, err := client.Lookup(context.Background(), "cached.com")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Status, result.Server)
}
Output: available cache
func WithUserAgent ¶
WithUserAgent sets the User-Agent header sent to HTTP endpoints.
type Outcome ¶
type Outcome string
Outcome is the conclusion a detector reached about a response.
const ( // OutcomeAvailable means the response reports no registration. OutcomeAvailable Outcome = "available" OutcomeUnavailable Outcome = "unavailable" // OutcomeUnsupportedTLD means the server said it does not serve this TLD. OutcomeUnsupportedTLD Outcome = "unsupported_tld" // out, so the response says nothing about the domain. OutcomeServerUnavailable Outcome = "server_unavailable" // OutcomeEmptyResponse means the response was blank, which says nothing about // the domain either. OutcomeEmptyResponse Outcome = "empty_response" )
type PatternDetector ¶
type PatternDetector struct {
// contains filtered or unexported fields
}
PatternDetector classifies answers by the wording registries use, running the checks in checks.go in order over the response.
It holds nothing but the tables it decides with and the steps it runs, both read-only, so one detector serves any number of concurrent lookups.
func DefaultDetector ¶
func DefaultDetector() *PatternDetector
DefaultDetector returns the shared detector the package-level functions use.
func NewPatternDetector ¶
func NewPatternDetector() *PatternDetector
NewPatternDetector returns a detector using DefaultRules.
func (*PatternDetector) Details ¶
func (d *PatternDetector) Details(response, tld string, registryMarkerMatched bool) Details
Details runs every check on its own and reports what each concluded, alongside the verdict Detect would reach. It is a debugging aid for a response that was classified wrongly, not something to branch on.
func (*PatternDetector) Detect ¶
func (d *PatternDetector) Detect(response, tld string, registryMarkerMatched bool) (bool, error)
Detect implements Detector.
The checks run in priority order, so that an answer holding both a "no match" phrase and a registration record reads as registered:
- A non-answer: blank, or a server that is busy, rate-limiting or not serving this TLD. These end the lookup with an error.
- Evidence the name is taken: the status fields of a registered domain, an RDAP domain document, or a registry notice withholding a reserved name.
- A registration record: enough whois fields that only a registered name carries them.
- The wordings that mean "no record", ignoring comment lines and legal boilerplate so that a notice mentioning them does not decide the verdict.
- The wording of the registry serving this TLD.
- An explicit status field, or an answer carrying none of the fields a registered domain would.
func (*PatternDetector) Rules ¶
func (d *PatternDetector) Rules() Rules
Rules returns the rules in use. The tables inside must not be modified; use Rules.Clone to derive a set that can be.
func (*PatternDetector) WithRules ¶
func (d *PatternDetector) WithRules(rules Rules) *PatternDetector
WithRules returns a detector using the given rules, leaving the receiver untouched. Start from DefaultRules and adjust, so that a partly filled Rules cannot leave the detector with empty tables:
rules := whois.DefaultRules().Clone() rules.TLDAvailability[".example"] = whois.CompileRules(`no such name`) detector := whois.NewPatternDetector().WithRules(rules)
type Patterns ¶
Patterns are wordings looked for as regular expressions, each made case-insensitive by CompileRules, so they match a response as it was sent.
func CompileRules ¶
CompileRules turns wordings into the case-insensitive patterns the tables hold. It panics on a malformed pattern, which can only be a mistake in the pattern itself, not a runtime condition.
type Phrases ¶
type Phrases []string
Phrases are wordings looked for as substrings. Every entry is lower case, and the text they are matched against is lower-cased first: a registry writes "NOT FOUND" as readily as "Not found", and folding once at the response is cheaper than folding at every entry.
type Registry ¶
type Registry interface {
// Definition returns the definition for a TLD, which may be given with or
// without a leading dot, and reports whether one exists.
Definition(tld string) (Definition, bool)
}
Registry resolves a TLD to the definition of the whois service answering for it. Definitions is the implementation this package bundles; supply another to read definitions from a database, a file watched for changes, or a service.
A Registry may be called from several goroutines at once.
type Response ¶
type Response struct {
// Body is the answer, unmodified.
Body string
// Server is the host:port or URL that answered.
Server string
// StatusCode is the HTTP status for an HTTP endpoint, and 0 for the WHOIS
// protocol, which has no status.
StatusCode int
}
Response is what a whois service answered.
type Result ¶
type Result struct {
// Domain is the validated name that was queried.
Domain Domain
// Status is the verdict: available, unavailable or premium.
Status Status
// Raw is the answer of the registry, unmodified.
Raw string
// Server is the host:port or URL that answered.
Server string
// Definition is the TLD definition the lookup was made with.
Definition Definition
}
Result is the outcome of a single lookup.
func Lookup ¶
Lookup queries the whois service for one domain with the default client. See Client.Lookup.
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
whois "github.com/monovm/whois-go"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
result, err := whois.Lookup(ctx, "monovm.com")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Domain, result.Status)
if !result.IsAvailable() {
fmt.Println(result.Raw)
}
}
Output:
Example (Errors) ¶
Telling a transient failure from a permanent one is what makes a retry worth attempting, and what keeps a busy registry from being read as an answer.
package main
import (
"context"
"errors"
"fmt"
whois "github.com/monovm/whois-go"
)
func main() {
_, err := whois.Lookup(context.Background(), "example.aninvalidtld")
switch {
case errors.Is(err, whois.ErrNoWhoisServer):
fmt.Println("no registry known for this tld")
case errors.Is(err, whois.ErrInvalidDomain):
fmt.Println("that is not a domain name")
case errors.Is(err, whois.ErrServerUnavailable):
fmt.Println("registry is busy, retry later")
case errors.Is(err, whois.ErrTLDNotSupported):
fmt.Println("the registry does not answer for this tld")
case err != nil:
fmt.Println("lookup failed:", err)
}
}
Output: no registry known for this tld
func (*Result) AvailabilityDetails ¶
AvailabilityDetails explains how the verdict was reached, for debugging a result that looks wrong. The verdict itself is Status.
func (*Result) IsAvailable ¶
IsAvailable reports whether the domain is free to register.
type Router ¶
type Router struct {
// Socket handles socket:// definitions.
Socket Transport
// HTTP handles every other definition.
HTTP Transport
}
Router sends each query over the transport its definition calls for: the WHOIS protocol for a socket:// endpoint, HTTP for anything else. It implements Transport by composing two of them.
type Rules ¶
type Rules struct {
// AvailabilityKeywords are the wordings for a name the registry holds no
// record of, matched against the response once comment lines and legal
// boilerplate have been dropped.
AvailabilityKeywords Phrases
// NoMatchPatterns are the same family of wordings as patterns, matched against
// the whole response.
NoMatchPatterns Patterns
// UnsupportedTLDPhrases and UnsupportedTLDPatterns mark a server saying it does
// not serve this TLD, which no retry will change.
UnsupportedTLDPhrases Phrases
UnsupportedTLDPatterns Patterns
// TemporaryFailurePhrases mark a server that will not answer for now: busy,
// rate-limiting or timing out. Splitting the two keeps every phrase in exactly
// one table, so that none can be a refusal without being classified, or
// classified without being a refusal.
TemporaryFailurePhrases Phrases
// and restriction notices that mean a name is taken.
UnavailabilityPatterns Patterns
// that is taken. Consulted before UnavailabilityPatterns.
TLDUnavailability TLDPatterns
// TLDAvailability holds, per TLD, the wording that registry uses for a name
// that is free. Consulted only after every unavailability check comes up empty.
TLDAvailability TLDPatterns
// RegistrationIndicators are the field names a filled-in registration record
// carries. MinRegistrationIndicators of them mean the name is registered.
RegistrationIndicators Phrases
MinRegistrationIndicators int
// RegistrationFields is the narrower set used to judge whether a response
// carries registration data at all. Fewer than MinRegistrationFields of them,
// in a response with no error or restriction marker, reads as a free name.
RegistrationFields Phrases
MinRegistrationFields int
// AvailabilityStatusIndicators are explicit status fields reporting a free name.
AvailabilityStatusIndicators Phrases
// ErrorOrRestrictionMarkers mark a response that withholds a record rather
// than reporting a free name, which is why an answer holding one of them is
// never read as available for lack of registration fields.
ErrorOrRestrictionMarkers Phrases
// ExplicitVerdictPhrases keep a short but conclusive answer from counting as
// too short to judge.
ExplicitVerdictPhrases Phrases
// MinRecordLength and MinRecordLines are the size below which a response is
// reported as too short to hold a registration record.
MinRecordLength int
MinRecordLines int
}
Rules are the wording tables and thresholds a PatternDetector classifies with. Injecting them keeps the decision logic free of the data it decides on: to teach the detector a registry it does not know, clone DefaultRules, add an entry, and build a detector from the result.
Example ¶
A registry whose wording the detector does not know is taught, without forking the package: clone the rules, add an entry, and build a detector from the result.
package main
import (
"fmt"
"log"
whois "github.com/monovm/whois-go"
)
func main() {
rules := whois.DefaultRules().Clone()
rules.TLDAvailability[".example"] = whois.CompileRules(`kein\s+eintrag`)
detector := whois.NewPatternDetector().WithRules(rules)
available, err := detector.Detect("Kein Eintrag fuer diesen Namen", ".example", false)
if err != nil {
log.Fatal(err)
}
fmt.Println(available)
}
Output: true
func DefaultRules ¶
func DefaultRules() Rules
DefaultRules returns the wording tables the package classifies answers with. The tables inside are shared between callers and must not be modified; use Rules.Clone to derive a set that can be.
type SocketTransport ¶
type SocketTransport struct {
// Dialer opens the connection. Nil means a plain dialer.
Dialer *net.Dialer
// Timeout bounds one query, connect and read together. Zero leaves the
// bound to the context alone.
Timeout time.Duration
// MaxResponseSize caps the answer. Zero means DefaultMaxResponseSize. An
// answer beyond the cap fails with ErrResponseTooLarge rather than being
// truncated, since a verdict from half an answer cannot be trusted.
MaxResponseSize int64
}
SocketTransport speaks the WHOIS protocol: it connects to the server, sends the domain name followed by a carriage return and a newline, and reads the answer until the server closes the connection.
The zero value works, using a plain dialer, no timeout beyond the context, and the default size cap.
func (*SocketTransport) Query ¶
func (t *SocketTransport) Query(ctx context.Context, domain string, def Definition) (*Response, error)
Query implements Transport.
type Status ¶
type Status string
Status is the outcome of a domain check.
const ( // StatusAvailable means the registry holds no record for the name. StatusAvailable Status = "available" StatusUnavailable Status = "unavailable" // StatusPremium means the registry marked the name as premium or reserved. StatusPremium Status = "premium" // StatusInvalid means the name could not be looked up at all: it is malformed, // or no registry is known for its TLD. StatusInvalid Status = "invalid" // StatusError means the lookup failed, so the state of the name is unknown. // It is not a statement about the name. StatusError Status = "error" )
type TLDPatterns ¶
TLDPatterns holds wordings per TLD, keyed as the definition table is: lower case, with a leading dot.
func (TLDPatterns) Lookup ¶
func (t TLDPatterns) Lookup(tld string) Patterns
Lookup returns what the table holds for a suffix, falling back to the last label of that suffix. Several registries are reachable only through a suffix of more than one label, ".co.ke" or ".co.th" among them, while their wording is recorded under the country code alone; without the fallback those entries could never be reached.
type Transport ¶
type Transport interface {
// Query sends a domain name to the service the definition describes. The
// domain arrives validated: lower case, ASCII, and free of any character
// that could alter the request.
Query(ctx context.Context, domain string, def Definition) (*Response, error)
}
Transport carries a query to a whois service and returns its answer.
Implementations must honour the context, must not modify the definition, and must be safe to call from several goroutines at once. Supply one to route queries through a proxy, to add a cache, or to answer from fixtures in a test.