hoverclient

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: May 26, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package hoverclient implements the Hover DNS provider client.

Hover ships no official API. This package mimics the browser-side authentication flow exposed by Hover's signin UI:

  1. POST https://www.hover.com/signin/auth.json (username, password).
  2. POST https://www.hover.com/signin/auth2.json (code) when MFA is required.
  3. Subsequent requests carry the session cookie jar.

TOTP codes are RFC 6238 (HMAC-SHA1, 30s window, 6 digits).

Index

Constants

This section is empty.

Variables

View Source
var ErrEmptyNameservers = errors.New("hover: delegation read returned 0 nameservers (verify field shape)")

ErrEmptyNameservers is returned by GetDomainDelegation when the parsed response has zero nameservers. Converts the silent-thrash failure mode (empty → Diff says NeedsUpdate forever → re-PUT loop) into a loud, single-iteration error visible at the first wfctl plan.

Functions

This section is empty.

Types

type Client

type Client struct {
	UserAgent string
	// contains filtered or unexported fields
}

Client is a Hover account-portal client. Concurrency-safe; the underlying cookie jar serialises across goroutines via mu.

func NewClient

func NewClient(creds Credentials, httpClient *http.Client) (*Client, error)

NewClient returns a fresh Client. Pass http=nil for an internal jar-backed http.Client. Tests inject a stub to redirect requests.

func (*Client) CreateRecord

func (c *Client) CreateRecord(ctx context.Context, domainID string, rec DNSRecord) (*DNSRecord, error)

CreateRecord adds a new DNS record for the domain.

func (*Client) DeleteRecord

func (c *Client) DeleteRecord(ctx context.Context, recordID string) error

DeleteRecord removes a record by ID.

func (*Client) GetDomain

func (c *Client) GetDomain(ctx context.Context, domain string) (*Domain, error)

GetDomain returns the full Domain struct (including the hover-assigned ID) for the named zone. The ID is required when creating new records via CreateRecord; the human-readable name is not accepted by the POST /api/dns endpoint.

func (*Client) GetDomainDelegation

func (c *Client) GetDomainDelegation(ctx context.Context, domainName string) (*DomainDelegation, error)

GetDomainDelegation fetches the registrar-level nameserver delegation for the named domain via the control-panel API (same endpoint family as the PUT used by SetNameservers — more likely to surface nameservers reliably than the DNS-records-oriented /api/domains/<name>/dns endpoint).

Returns ErrEmptyNameservers if the parsed response has zero nameservers. This loud-on-empty behavior is intentional: it converts the silent re-apply thrash failure mode (empty → Diff says NeedsUpdate forever) into a single-iteration error visible at first wfctl plan.

func (*Client) ListRecords

func (c *Client) ListRecords(ctx context.Context, domain string) ([]DNSRecord, error)

ListRecords returns records for the named zone. Caller MUST pass the apex domain (e.g. "example.com").

func (*Client) Login

func (c *Client) Login(ctx context.Context) error

Login performs a full authentication cycle against Hover's control panel. It is safe to call when already authenticated — it re-authenticates only when the session is older than sessionStaleAfter (1 hour). Safe for concurrent use; the internal mutex serialises calls.

The underlying auth flow follows Hover's current React signin UI:

  1. POST https://www.hover.com/signin/auth.json with username + password.
  2. If the response status is "need_2fa", POST /signin/auth2.json with the current TOTP code.
  3. Session cookies are stored in the jar for subsequent API calls.

func (*Client) SetNameservers

func (c *Client) SetNameservers(ctx context.Context, domainName string, ns []string) error

SetNameservers updates the registrar-level nameservers for a domain via Hover's control-panel API.

Lock discipline: holds c.mu for the entire auth → CSRF fetch → PUT sequence. This eliminates the TOCTOU window between auth-check and PUT (another goroutine cannot re-auth and invalidate the CSRF token between the two requests).

Trade-off: any concurrent caller using the same *Client blocks for the full duration of the held-lock sequence. Worst case (session is stale and re-auth fires inside ensureLoginLocked):

  • POST /signin/auth.json (credentials)
  • POST /signin/auth2.json (TOTP code, only if MFA enabled)
  • GET /control_panel/domain/<name> (CSRF for the API write)
  • PUT /api/control_panel/domains/domain-<name>

Up to ~180s at the 30s default per-request timeout when re-auth is needed; ~60s on the warm-session path (CSRF GET + PUT). Acceptable for the field-test scope (single goroutine, one delegation resource). Future: cache CSRF at session granularity if mixed-resource throughput becomes a concern.

func (*Client) UpdateRecord

func (c *Client) UpdateRecord(ctx context.Context, recordID string, rec DNSRecord) error

UpdateRecord PATCHes an existing record's content (and TTL when > 0).

type Credentials

type Credentials struct {
	Username   string
	Password   string
	TOTPSecret TOTPSecret
}

Credentials carries the operator-provided login material.

type DNSRecord

type DNSRecord struct {
	ID      string `json:"id,omitempty"`
	Type    string `json:"type"`
	Name    string `json:"name"`
	Content string `json:"content"`
	TTL     int    `json:"ttl,omitempty"`
}

DNSRecord mirrors Hover's internal API record shape.

type Domain

type Domain struct {
	ID      string      `json:"id"`
	Name    string      `json:"domain_name"`
	Records []DNSRecord `json:"entries"`
}

Domain is the API shape returned by GET /api/domains.

type DomainDelegation

type DomainDelegation struct {
	ID          string   `json:"id"`
	Name        string   `json:"domain_name"`
	Nameservers []string `json:"nameservers"`
}

DomainDelegation is the response shape of GET /api/control_panel/domains/domain-<name>. Distinct from Domain (which represents the /api/domains/<name>/dns shape with Records) to avoid ambiguity over which fields are populated by which endpoint.

Tentative envelope per design A6: flat object, not wrapped in {"domains":[...]}. First field-test call must confirm this shape; if Hover returns a different envelope the implementer pauses and amends the design before proceeding.

type TOTPSecret

type TOTPSecret struct {
	// contains filtered or unexported fields
}

TOTPSecret holds a decoded HOTP key. Construct via ParseBase32.

func ParseBase32

func ParseBase32(seed string) (TOTPSecret, error)

ParseBase32 parses a Google-Authenticator-style base32 seed (case- insensitive, padding optional) into a TOTPSecret. Spaces are stripped so users can paste from the Hover 2FA setup dialog.

func (TOTPSecret) Code

func (s TOTPSecret) Code() string

Code returns the 6-digit code for the current wall-clock time.

func (TOTPSecret) CodeAt

func (s TOTPSecret) CodeAt(t int64) string

CodeAt returns the 6-digit code for the given Unix time t (seconds). 30-second step per RFC 6238 §4. Pure HMAC-SHA1 — no external deps.

Jump to

Keyboard shortcuts

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