happyDomain/checkers/domain_expiry.go
Pierre-Olivier Mercier 5de221411e
Some checks failed
continuous-integration/drone/push Build is failing
New checker: domain lock status
2026-04-16 15:22:52 +07:00

237 lines
6.9 KiB
Go

// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checkers
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
"git.happydns.org/happyDomain/internal/checker"
"git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/pkg/domaininfo"
)
const (
// ObservationKeyWhois is the observation key for WHOIS / domain expiry data.
ObservationKeyWhois happydns.ObservationKey = "whois"
defaultWarningDays = 30
defaultCriticalDays = 7
)
// WHOISData represents WHOIS observation data.
type WHOISData struct {
ExpiryDate time.Time `json:"expiryDate"`
Registrar string `json:"registrar"`
Contacts map[string]*happydns.ContactInfo `json:"contacts,omitempty"`
Status []string `json:"status,omitempty"`
}
// whoisProvider is a placeholder WHOIS observation provider.
type whoisProvider struct{}
func (p *whoisProvider) Key() happydns.ObservationKey {
return ObservationKeyWhois
}
func (p *whoisProvider) Collect(ctx context.Context, opts happydns.CheckerOptions) (any, error) {
domainName, _ := opts["domainName"].(string)
if domainName == "" {
return nil, fmt.Errorf("domainName is required")
}
info, err := domaininfo.GetDomainInfo(ctx, happydns.Origin(domainName))
if err != nil {
return nil, fmt.Errorf("failed to retrieve domain info: %w", err)
}
if info.ExpirationDate == nil {
return nil, fmt.Errorf("expiration date not available for %s", domainName)
}
registrar := info.Registrar
if registrar == "" {
registrar = "Unknown"
}
return &WHOISData{
ExpiryDate: *info.ExpirationDate,
Registrar: registrar,
Contacts: info.Contacts,
Status: info.Status,
}, nil
}
// ExtractMetrics implements happydns.CheckerMetricsReporter.
func (p *whoisProvider) ExtractMetrics(raw json.RawMessage, collectedAt time.Time) ([]happydns.CheckMetric, error) {
var data WHOISData
if err := json.Unmarshal(raw, &data); err != nil {
return nil, err
}
daysRemaining := data.ExpiryDate.Sub(collectedAt).Hours() / 24
return []happydns.CheckMetric{{
Name: "domain_expiry_days_remaining",
Value: daysRemaining,
Unit: "days",
Labels: map[string]string{"registrar": data.Registrar},
Timestamp: collectedAt,
}}, nil
}
// domainExpiryRule checks whether a domain is nearing expiration.
type domainExpiryRule struct{}
func (r *domainExpiryRule) Name() string {
return "domain_expiry_check"
}
func (r *domainExpiryRule) Description() string {
return "Checks whether a domain name is nearing its expiration date"
}
func (r *domainExpiryRule) ValidateOptions(opts happydns.CheckerOptions) error {
warningDays := float64(defaultWarningDays)
criticalDays := float64(defaultCriticalDays)
if v, ok := opts["warning_days"]; ok {
d, ok := v.(float64)
if !ok {
return fmt.Errorf("warning_days must be a number")
}
if d <= 0 {
return fmt.Errorf("warning_days must be positive")
}
warningDays = d
}
if v, ok := opts["critical_days"]; ok {
d, ok := v.(float64)
if !ok {
return fmt.Errorf("critical_days must be a number")
}
if d <= 0 {
return fmt.Errorf("critical_days must be positive")
}
criticalDays = d
}
if criticalDays >= warningDays {
return fmt.Errorf("critical_days (%v) must be less than warning_days (%v)", criticalDays, warningDays)
}
return nil
}
func (r *domainExpiryRule) Evaluate(ctx context.Context, obs happydns.ObservationGetter, opts happydns.CheckerOptions) happydns.CheckState {
var whois WHOISData
if err := obs.Get(ctx, ObservationKeyWhois, &whois); err != nil {
return happydns.CheckState{
Status: happydns.StatusError,
Message: fmt.Sprintf("Failed to get WHOIS data: %v", err),
Code: "whois_error",
}
}
// Read thresholds from options with defaults.
warningDays := sdk.GetIntOption(opts, "warning_days", defaultWarningDays)
criticalDays := sdk.GetIntOption(opts, "critical_days", defaultCriticalDays)
daysRemaining := int(time.Until(whois.ExpiryDate).Hours() / 24)
meta := map[string]any{"days_remaining": daysRemaining}
if daysRemaining <= criticalDays {
return happydns.CheckState{
Status: happydns.StatusCrit,
Message: fmt.Sprintf("Domain expires in %d days", daysRemaining),
Code: "expiry_critical",
Meta: meta,
}
}
if daysRemaining <= warningDays {
return happydns.CheckState{
Status: happydns.StatusWarn,
Message: fmt.Sprintf("Domain expires in %d days", daysRemaining),
Code: "expiry_warning",
Meta: meta,
}
}
return happydns.CheckState{
Status: happydns.StatusOK,
Message: fmt.Sprintf("Domain expires in %d days", daysRemaining),
Code: "expiry_ok",
Meta: meta,
}
}
func init() {
checker.RegisterObservationProvider(&whoisProvider{})
checker.RegisterChecker(&happydns.CheckerDefinition{
ID: "domain_expiry",
Name: "Domain Expiry",
Availability: happydns.CheckerAvailability{
ApplyToDomain: true,
},
ObservationKeys: []happydns.ObservationKey{ObservationKeyWhois},
Options: happydns.CheckerOptionsDocumentation{
DomainOpts: []happydns.CheckerOptionDocumentation{
{
Id: "domainName",
Type: "string",
AutoFill: happydns.AutoFillDomainName,
Hide: true,
},
{
Id: "warning_days",
Type: "uint",
Label: "Warning threshold (days)",
Description: "Number of days before expiration to trigger a warning.",
Default: defaultWarningDays,
Placeholder: strconv.Itoa(defaultWarningDays),
},
{
Id: "critical_days",
Type: "uint",
Label: "Critical threshold (days)",
Description: "Number of days before expiration to trigger a critical alert.",
Default: defaultCriticalDays,
Placeholder: strconv.Itoa(defaultCriticalDays),
},
},
},
Rules: []happydns.CheckRule{
&domainExpiryRule{},
},
Interval: &happydns.CheckIntervalSpec{
Min: 12 * time.Hour,
Max: 7 * 24 * time.Hour,
Default: 24 * time.Hour,
},
HasMetrics: true,
})
}