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:
- POST https://www.hover.com/signin/auth.json (username, password).
- POST https://www.hover.com/signin/auth2.json (code) when MFA is required.
- Subsequent requests carry the session cookie jar.
TOTP codes are RFC 6238 (HMAC-SHA1, 30s window, 6 digits).
Index ¶
- Variables
- type Client
- func (c *Client) CreateRecord(ctx context.Context, domainID string, rec DNSRecord) (*DNSRecord, error)
- func (c *Client) DeleteRecord(ctx context.Context, recordID string) error
- func (c *Client) GetDomain(ctx context.Context, domain string) (*Domain, error)
- func (c *Client) GetDomainDelegation(ctx context.Context, domainName string) (*DomainDelegation, error)
- func (c *Client) ListRecords(ctx context.Context, domain string) ([]DNSRecord, error)
- func (c *Client) Login(ctx context.Context) error
- func (c *Client) SetNameservers(ctx context.Context, domainName string, ns []string) error
- func (c *Client) UpdateRecord(ctx context.Context, recordID string, rec DNSRecord) error
- type Credentials
- type DNSRecord
- type Domain
- type DomainDelegation
- type TOTPSecret
Constants ¶
This section is empty.
Variables ¶
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 ¶
DeleteRecord removes a record by ID.
func (*Client) GetDomain ¶
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 ¶
ListRecords returns records for the named zone. Caller MUST pass the apex domain (e.g. "example.com").
func (*Client) Login ¶
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:
- POST https://www.hover.com/signin/auth.json with username + password.
- If the response status is "need_2fa", POST /signin/auth2.json with the current TOTP code.
- Session cookies are stored in the jar for subsequent API calls.
func (*Client) SetNameservers ¶
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.
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.