whois

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 17 Imported by: 0

README

whois-go

Go Reference CI Go Report Card

Domain WHOIS lookup and availability checking in Go. Around 880 TLDs are bundled with the package, queried over WHOIS 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 answers are classified by a detector built from the wording each one uses: status fields, "no match" phrasings in several languages, RDAP documents, and the restriction notices that look empty but mean the name is taken.

A failure is never a verdict. A registry that is busy, rate-limiting or silent tells you nothing about a name, and this package says so with an error instead of guessing. That is the single most common way an availability check starts lying.

This is the Go port of monovm/whois-php. Requires Go 1.25 or later; the only dependency is golang.org/x/net/idna, for internationalized names.

Install

go get github.com/monovm/whois-go
import whois "github.com/monovm/whois-go"

Quick start

package main

import (
	"context"
	"fmt"
	"log"

	whois "github.com/monovm/whois-go"
)

func main() {
	result, err := whois.Lookup(context.Background(), "monovm.com")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Status)        // unavailable
	fmt.Println(result.IsAvailable()) // false
	fmt.Println(result.Raw)           // the registry answer, unmodified
}

Checking several names

Check takes any number of names and queries them concurrently. A name given without a TLD is expanded over the popular TLDs:

statuses := whois.Check(context.Background(), "monovm.com", "example")

// map[string]whois.Status{
//   "monovm.com":   "unavailable",
//   "example.com":  "unavailable",
//   "example.net":  "unavailable",
//   "example.org":  "unavailable",
//   "example.info": "unavailable",
// }

Check never returns an error: a name that could not be checked gets StatusInvalid (malformed, or no registry known for its TLD) or StatusError (the lookup failed). Use CheckAll to see why, in the order the names were expanded:

for _, r := range whois.CheckAll(ctx, "monovm.com", "example.aninvalidtld") {
	if r.Err != nil {
		// Not a verdict: the name may well be registered.
		fmt.Printf("%s: could not check: %v\n", r.Domain, r.Err)
		continue
	}
	fmt.Printf("%s: %s\n", r.Domain, r.Status)
}

Statuses

Status Meaning
StatusAvailable The registry holds no record for the name.
StatusUnavailable The name is registered.
StatusPremium The registry marked the name as premium or reserved.
StatusInvalid The name is malformed, or no registry is known for its TLD.
StatusError The lookup failed, so the state of the name is unknown.

Internationalized names

A name in any script is converted to the ASCII form registries expect, as IDNA2008 defines it:

result, _ := whois.Lookup(ctx, "münchen.de")
fmt.Println(result.Domain) // xn--mnchen-3ya.de

ParseDomain is the same conversion on its own, and it is also the validation every lookup goes through: a name carrying a newline, a #, a ? or anything else that could alter the WHOIS request or the endpoint URL it goes into is refused before it reaches the wire.

domain, err := whois.ParseDomain("example.com\r\nsecond.com")
// err matches whois.ErrInvalidDomain

Errors

Lookups tell a transient failure from a permanent one, so a caller can decide whether a retry is worth it:

result, err := whois.Lookup(ctx, domain)
switch {
case errors.Is(err, whois.ErrInvalidDomain):
	// Not a domain name: empty, no TLD, or an impossible character.
case errors.Is(err, whois.ErrNoWhoisServer):
	// No registry is known for this TLD.
case errors.Is(err, whois.ErrServerUnavailable):
	// The registry is busy or rate-limiting: retry later.
case errors.Is(err, whois.ErrTLDNotSupported):
	// The server answered that it does not serve this TLD.
case errors.Is(err, whois.ErrEmptyResponse):
	// The server closed the connection without answering.
case errors.Is(err, whois.ErrResponseTooLarge):
	// The answer passed the size cap and was not judged.
case err != nil:
	// Network failure, cancelled context, and so on.
}

Configuring a client

The package-level functions use a shared client with the defaults. Build your own to change them:

client, err := whois.NewClient(
	whois.WithTimeout(5*time.Second),
	whois.WithConcurrency(16),
	whois.WithPopularTLDs(".com", ".dev", ".io"),
)
if err != nil {
	log.Fatal(err)
}

result, err := client.Lookup(ctx, "monovm.dev")
Option Default
WithTimeout 10s per WHOIS query, 60s per HTTP query
WithHTTPTimeout 60s
WithConcurrency 8 names at a time
WithPopularTLDs .com, .net, .org, .info
WithMaxResponseSize 1 MiB
WithDefinitions, WithDefinitionOverrides the bundled TLD table
WithRegistry, WithTransport, WithDetector the implementations described below
WithDialer, WithHTTPClient, WithUserAgent standard library defaults
WithInsecureTLS off, certificates are verified

Options describe a configuration rather than mutate a half-built client, so the order they are given in never changes the result. A Client is safe for concurrent use, and one per process is enough.

Composition

A lookup is three steps, each an interface with a default implementation and an option to replace it. Nothing needs forking to be adapted.

Client
 ├── Registry   tld            → Definition        (Definitions, or your own)
 ├── Transport  domain, def    → Response          (Router → SocketTransport | HTTPTransport)
 └── Detector   response, tld  → available, error  (PatternDetector, driven by Rules)
Interface Bundled implementation Replace it to
Registry Definitions, from the embedded table read definitions from a database or a live file
Transport Router, over SocketTransport and HTTPTransport add a cache, a proxy, a rate limiter, or fixtures
Detector PatternDetector, classifying by the wording in Rules classify with rules of your own

A transport is the seam a test or a cache goes through:

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

client, err := whois.NewClient(whois.WithTransport(&cachingTransport{
	answers: answers,
	inner:   &whois.SocketTransport{Timeout: 10 * time.Second},
}))

Overriding TLD definitions

Definitions live in data/dist.whois.json: one entry per group of TLDs, with the endpoint and the registry's own "not registered" wording. To point a TLD elsewhere, or to add one, merge your own entries over the bundled table:

overrides := []byte(`[
  {"extensions": ".example,.test", "uri": "socket://whois.example.test", "available": "No match"}
]`)

client, err := whois.NewClient(whois.WithDefinitionOverrides(overrides))

uri is either socket://host[:port] for WHOIS over TCP, port 43 by default, or an HTTP(S) URL the domain name is appended to. A marker written with a leading --- is anchored to the start of the answer, which is how .au, .sg, .ch and .io tell "Available" on the first line from the same word further down. This is the counterpart of the PHP package's whois.json override file.

Classifying an answer you already have

The detector is usable on its own, for an answer fetched elsewhere or stored:

available, err := whois.DetectAvailability(response, ".com", false)

When a verdict looks wrong, AvailabilityDetails reports which checks fired:

details := whois.AvailabilityDetails(response, ".de", false)

fmt.Println(details.Outcome)                          // unavailable
fmt.Println(details.ContainsUnavailabilityIndicators) // true
fmt.Println(details.ResponsePreview)                  // first 200 characters

Checks run in priority order, so that an answer holding both a "no match" phrase and a registration record reads as registered:

  1. Non-answers — blank, or a server that is busy, rate-limiting, or not serving this TLD. These end the lookup with an error rather than a verdict.
  2. Evidence the name is taken — registry status fields (Status: connect, Domain Status: clientTransferProhibited), RDAP domain documents, and notices withholding a reserved name such as usage restrictions applied.
  3. A registration record — three or more whois fields that only a registered name carries.
  4. "No match" wordings — in several languages, ignoring comment lines and legal boilerplate so that a notice mentioning them does not decide the verdict.
  5. Registry-specific wordings — per-TLD patterns for the registries that phrase it their own way, reachable through a suffix of more than one label as well (.ke applies to .co.ke).
  6. Status fields and missing records — an explicit Status: available, or an answer carrying none of the fields a registered name would.

To teach it a registry it does not know, clone the rules and add an entry:

rules := whois.DefaultRules().Clone()
rules.TLDAvailability[".example"] = whois.CompileRules(`kein\s+eintrag`)

detector := whois.NewPatternDetector().WithRules(rules)
client, err := whois.NewClient(whois.WithDetector(detector))

Command line

go install github.com/monovm/whois-go/cmd/whois@latest
$ whois monovm.com example.org notregistered-93f1c0.com
monovm.com                unavailable
example.org               unavailable
notregistered-93f1c0.com  available

$ whois -tlds .com,.dev,.io monovm
monovm.com  unavailable
monovm.dev  available
monovm.io   unavailable

$ whois -json monovm.com
[
  {
    "domain": "monovm.com",
    "status": "unavailable",
    "available": false,
    "server": "whois.crsnic.net:43",
    "checked_at": "2026-08-03T10:15:00Z"
  }
]

-raw prints the registry answer, and -timeout, -concurrency and -insecure mirror the client options. The command exits with status 1 if any name could not be checked.

Differences from whois-php

Same TLD table and the same detection logic, so the two agree on verdicts. What changed is the surface, to match what Go callers expect, and six defects that were fixed along the way.

Surface

  • Errors instead of exceptions. A failed lookup returns a wrapped sentinel error to test with errors.Is, rather than an exception or a false return.
  • Raw answers. Result.Raw is what the registry sent. The PHP package HTML-escapes it and inserts <br /> tags, then undoes that before inspecting it.
  • One detection pass. The PHP handler runs the detector twice, once on the answer and once on a message it built from the first verdict, which reports premium names as available. Here the detector runs once, on the answer.
  • Concurrent batches. Check queries up to 8 names at a time instead of one after another, and keeps the order of the names it was given.
  • Context support. Every lookup takes a context.Context.
  • Names are validated and normalized. Lower-cased, trailing dot removed, converted from any script to punycode, and refused if they hold a character that could alter the request. The PHP package passes the input through as it is.
  • TLS verified by default. The PHP package skips certificate verification for every HTTP endpoint. Use WithInsecureTLS(true) for the few registries with broken certificates.
  • Composable. The registry, the transport and the detector are interfaces.

Defects fixed

  • A rate-limited registry is no longer read as an available domain. GoDaddy Registry (.us, .biz, .tv, .club, .vip, .nyc) answers "Number of allowed queries exceeded.", AFNIC answers "%% WHOIS LIMIT EXCEEDED", NASK answers "request limit exceeded" and nic.at answers "% Quota exceeded". None carries registration fields, so the PHP detector reports the name as free — including nic.club and nic.pl, which are registered. Here they return ErrServerUnavailable.
  • An empty answer is no longer read as available. PHP reads silence as a free name; here it is ErrEmptyResponse.
  • The ---not found contradiction. PHP lists ---not found and ---domain not found as evidence that a name is taken, while the same two wordings also appear in its availability table — and unavailability is checked first. A registry answering "Domain not found." on the first line therefore reads as registered the moment its definition's own marker stops matching, which is what .au does today (its marker is "Available", auDA now answers "Domain not found."). Those two entries are gone.
  • The .uk rule no longer matches "has not been registered". PHP matches the bare word registered for .uk, which is inside the wording Nominet answers a free name with.
  • 404 is matched as a status, not as a street number. PHP matches a bare 404 anywhere in an answer, including in a registrant's address.
  • Registry rules reach suffixes of more than one label. Around sixty rules were keyed on a country code (.ke, .th, .tz) that the definition table only serves through a longer suffix (.co.ke, .co.th, .co.tz), so they could never be consulted.

Accuracy

WHOIS answers are wording, not data. This package classifies around 880 TLDs from the wording each registry uses, and registries do change it. If you find a TLD reported wrongly, AvailabilityDetails shows which check decided it — a report or a pull request with the answer attached is welcome.

For decisions that carry money, confirm the verdict with your registrar's API. The detector is written so that anything it cannot read becomes an error rather than a guess, but an out-of-date definition or a registry that reworded its answers will still show up as a wrong verdict.

Testing

go test ./...                      # unit tests, no network
go test -tags integration ./...    # queries live registries
go test -run XXX -fuzz FuzzParseDomain -fuzztime 30s .

The unit tests run against fixtures captured from real registries, with local WHOIS and RDAP servers standing in for the network, and cover roughly 97% of the package. The fuzz targets assert what must hold for any input: a name that parses can never carry a character that alters a request, and an answer that produces an error can never report a domain as available.

Source layout

One package, with the files following the three parts a lookup is made of. Within each part the decision, the data it decides on, and the text it is read from live apart, so that a change to a registry's wording never touches the code that classifies it.

File What it holds
whois.go the package-level functions, over a client shared by the process
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 Response
transport_socket.go, transport_http.go 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, details.go, errors.go what a lookup reports, and why
data/dist.whois.json the embedded TLD table
cmd/whois/ the command line tool

Each test file covers the file it is named after, stubs_test.go holds the fakes they share, and fixtures_test.go holds the registry answers they are written against.

Inside the detector
PatternDetector ──▶ checks.go   the six steps, in the order they run
                       │
                       ▼
                    scan.go     the response under inspection: one value that
                       │        answers "is this a record?", "was this a refusal?"
                       ▼
                    rules.go    Phrases and Patterns, which do their own matching

The classification is a list, not a cascade of conditions: each step carries the reason it runs where it does, TestClassificationOrder pins the order down, and TestEveryStepDecidesForSomeAnswer proves no step is shadowed by the one before it. Teaching the detector a new registry behaviour is a table extended or a step added — never an edit to a condition that six other answers also depend on.

Contributing

To add a TLD, extend the detector, or fix a wrong verdict, open a pull request at github.com/monovm/whois-go. For a detection fix, please include the registry answer that was misread, so it can be added to the fixtures.

License

MIT

Support

For support, email dev@monovm.comMonoVM.com

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

Examples

Constants

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

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

	// ErrServerUnavailable is returned when the server answered with a busy,
	// 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

func CanLookup(tld string) bool

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

func Check(ctx context.Context, domains ...string) map[string]Status

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

func DetectAvailability

func DetectAvailability(response, tld string, registryMarkerMatched bool) (bool, error)

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

func SplitDomain(domain string) (sld, tld string)

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.

func CheckAll

func CheckAll(ctx context.Context, domains ...string) []CheckResult

CheckAll is Check with the full result and error of every lookup. See Client.CheckAll.

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

func DefaultClient() (*Client, error)

DefaultClient returns the client the package-level functions use: the bundled TLD table, the standard transports and the default detector.

func NewClient

func NewClient(opts ...Option) (*Client, error)

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

func (c *Client) CanLookup(tld string) bool

CanLookup reports whether a whois service is known for the TLD, which may be given with or without a leading dot.

func (*Client) Check

func (c *Client) Check(ctx context.Context, domains ...string) map[string]Status

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

func (*Client) Lookup

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

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

func (c *Client) LookupDomain(ctx context.Context, domain Domain) (*Result, error)

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

func (c *Client) PopularTLDs() []string

PopularTLDs returns the TLDs a name given without one is expanded over.

func (*Client) Registry

func (c *Client) Registry() Registry

Registry returns the source of TLD definitions in use.

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"`
	// ContainsUnavailabilityIndicators is true when the response holds the status
	// 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

func AvailabilityDetails(response, tld string, registryMarkerMatched bool) Details

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

func ParseDomain(input string) (Domain, error)

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

func (Domain) IsZero

func (d Domain) IsZero() bool

IsZero reports whether the domain is the unusable zero value.

func (Domain) SLD

func (d Domain) SLD() string

SLD returns the second-level part, for example "example".

func (Domain) String

func (d Domain) String() string

String returns the full domain name, for example "example.co.uk".

func (Domain) TLD

func (d Domain) TLD() string

TLD returns the top-level part with its leading dot, for example ".co.uk".

func (Domain) WithTLD

func (d Domain) WithTLD(tld string) (Domain, error)

WithTLD returns the same second-level part under another TLD, which is how a name given without one is expanded over the popular TLDs. The TLD may be written with or without a leading dot.

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

func WithConcurrency(n int) Option

WithConcurrency sets how many names a batch queries at once.

func WithDefinitionOverrides

func WithDefinitionOverrides(data []byte) Option

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

func WithDetector(detector Detector) Option

WithDetector replaces the availability detector.

func WithDialer

func WithDialer(dialer *net.Dialer) Option

WithDialer sets the dialer used for the WHOIS protocol, for instance to pick a source address or a resolver.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient sets the client used for HTTP and RDAP endpoints.

func WithHTTPTimeout

func WithHTTPTimeout(timeout time.Duration) Option

WithHTTPTimeout bounds one query against an HTTP or RDAP endpoint, leaving the WHOIS timeout alone.

func WithInsecureTLS

func WithInsecureTLS(insecure bool) Option

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

func WithMaxResponseSize(n int64) Option

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

func WithPopularTLDs(tlds ...string) Option

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

func WithRegistry(registry Registry) Option

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

func WithTimeout(timeout time.Duration) Option

WithTimeout bounds one query. It applies to both transports; pass zero to leave the bound to the context alone.

func WithTransport

func WithTransport(transport Transport) Option

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

func WithUserAgent(userAgent string) Option

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 means the response reports a registration.
	OutcomeUnavailable Outcome = "unavailable"
	// OutcomeUnsupportedTLD means the server said it does not serve this TLD.
	OutcomeUnsupportedTLD Outcome = "unsupported_tld"
	// OutcomeServerUnavailable means the server was busy, rate-limiting or timing
	// 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"
)

func (Outcome) String

func (o Outcome) String() string

String implements fmt.Stringer.

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:

  1. A non-answer: blank, or a server that is busy, rate-limiting or not serving this TLD. These end the lookup with an error.
  2. Evidence the name is taken: the status fields of a registered domain, an RDAP domain document, or a registry notice withholding a reserved name.
  3. A registration record: enough whois fields that only a registered name carries them.
  4. The wordings that mean "no record", ignoring comment lines and legal boilerplate so that a notice mentioning them does not decide the verdict.
  5. The wording of the registry serving this TLD.
  6. 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

type Patterns []*regexp.Regexp

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

func CompileRules(patterns ...string) Patterns

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.

func (Patterns) Match

func (p Patterns) Match(s string) bool

Match reports whether any pattern matches the text.

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

func Lookup(ctx context.Context, domain string) (*Result, error)

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

func (r *Result) AvailabilityDetails() Details

AvailabilityDetails explains how the verdict was reached, for debugging a result that looks wrong. The verdict itself is Status.

func (*Result) IsAvailable

func (r *Result) IsAvailable() bool

IsAvailable reports whether the domain is free to register.

func (*Result) IsPremium

func (r *Result) IsPremium() bool

IsPremium reports whether the registry marked the name as premium or reserved.

func (*Result) Message

func (r *Result) Message() string

Message returns a line for a free name and the registry answer for a taken one. It is the counterpart of the PHP package's getWhoisMessage.

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.

func (Router) Query

func (r Router) Query(ctx context.Context, domain string, def Definition) (*Response, error)

Query implements Transport.

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

	// UnavailabilityPatterns are the registry-independent wordings, status values
	// and restriction notices that mean a name is taken.
	UnavailabilityPatterns Patterns
	// TLDUnavailability holds, per TLD, the wording that registry uses for a name
	// 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.

func (Rules) Clone

func (r Rules) Clone() Rules

Clone returns a deep copy of the rules, safe to modify without affecting the original or any detector using it.

func (Rules) TLDs

func (r Rules) TLDs() []string

TLDs returns every TLD either registry-specific table covers, sorted. It is what a detector can classify by registry wording rather than by the general tables alone.

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 means the name is registered.
	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"
)

func (Status) String

func (s Status) String() string

String implements fmt.Stringer.

type TLDPatterns

type TLDPatterns map[string]Patterns

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.

Directories

Path Synopsis
cmd
whois command
Command whois reports whether domains are registered.
Command whois reports whether domains are registered.

Jump to

Keyboard shortcuts

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