Initial code commit

This commit is contained in:
Warezpeddler
2026-01-29 00:03:02 +00:00
commit 8f35bb7ec8
38 changed files with 6039 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
package hetzner
import (
"fmt"
"time"
"github.com/go-resty/resty/v2"
)
const (
hetznerAPIBaseURL = "https://api.hetzner.cloud/v1"
)
type Client struct {
apiKey string
resty *resty.Client
}
type Server struct {
ID int `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
PublicNet PublicNet `json:"public_net"`
}
type PublicNet struct {
IPv4 IPv4Info `json:"ipv4"`
}
type IPv4Info struct {
IP string `json:"ip"`
}
type ServerResponse struct {
Server Server `json:"server"`
}
type ServersResponse struct {
Servers []Server `json:"servers"`
}
type CreateServerRequest struct {
Name string `json:"name"`
ServerType string `json:"server_type"`
Image string `json:"image"`
Location string `json:"location,omitempty"`
SSHKeys []int `json:"ssh_keys,omitempty"`
UserData string `json:"user_data,omitempty"`
Networks []int `json:"networks,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
func NewClient(apiKey string) *Client {
client := resty.New()
client.SetBaseURL(hetznerAPIBaseURL)
client.SetHeader("Authorization", fmt.Sprintf("Bearer %s", apiKey))
client.SetTimeout(30 * time.Second)
return &Client{
apiKey: apiKey,
resty: client,
}
}
func (c *Client) CreateServer(req CreateServerRequest) (*Server, error) {
var resp ServerResponse
httpResp, err := c.resty.R().
SetBody(req).
SetResult(&resp).
Post("/servers")
if err != nil {
return nil, fmt.Errorf("failed to create server: %w", err)
}
if httpResp.IsError() {
return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body()))
}
return &resp.Server, nil
}
func (c *Client) GetServer(serverID int) (*Server, error) {
var resp ServerResponse
httpResp, err := c.resty.R().
SetResult(&resp).
Get(fmt.Sprintf("/servers/%d", serverID))
if err != nil {
return nil, fmt.Errorf("failed to get server: %w", err)
}
if httpResp.IsError() {
return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body()))
}
return &resp.Server, nil
}
func (c *Client) DeleteServer(serverID int) error {
httpResp, err := c.resty.R().
Delete(fmt.Sprintf("/servers/%d", serverID))
if err != nil {
return fmt.Errorf("failed to delete server: %w", err)
}
if httpResp.IsError() {
return fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body()))
}
return nil
}
func (c *Client) WaitForServerReady(serverID int, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
server, err := c.GetServer(serverID)
if err != nil {
return fmt.Errorf("failed to check server status: %w", err)
}
if server.Status == "running" {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for server to be ready (current status: %s)", server.Status)
}
case <-time.After(timeout):
return fmt.Errorf("timeout waiting for server to be ready")
}
}
}
func (c *Client) GetServerIP(serverID int) (string, error) {
server, err := c.GetServer(serverID)
if err != nil {
return "", err
}
if server.PublicNet.IPv4.IP == "" {
return "", fmt.Errorf("server has no public IP address")
}
return server.PublicNet.IPv4.IP, nil
}
func (c *Client) ListServers() ([]Server, error) {
var resp ServersResponse
httpResp, err := c.resty.R().
SetResult(&resp).
Get("/servers")
if err != nil {
return nil, fmt.Errorf("failed to list servers: %w", err)
}
if httpResp.IsError() {
return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body()))
}
return resp.Servers, nil
}

View File

@@ -0,0 +1,76 @@
package hetzner
import (
"fmt"
"time"
)
type ServerInfo struct {
ID int
Name string
Status string
PublicIP string
Region string
Type string
}
func (c *Client) CreateServerWithConfig(name, serverType, location, image, userData string, sshKeyIDs []int) (*ServerInfo, error) {
if location == "" {
return nil, fmt.Errorf("location is required")
}
validLocations := map[string]bool{
"fsn1": true, // Falkenstein, Germany
"nbg1": true, // Nuremberg, Germany
"hel1": true, // Helsinki, Finland
"ash": true, // Ashburn, Virginia
"hil": true, // Hillsboro, Oregon
"sin": true, // Singapore
}
if !validLocations[location] {
return nil, fmt.Errorf("invalid location: %s (valid locations: fsn1, nbg1, hel1, ash, hil, sin)", location)
}
req := CreateServerRequest{
Name: name,
ServerType: serverType,
Image: image,
Location: location,
UserData: userData,
SSHKeys: sshKeyIDs,
Labels: map[string]string{
"managed-by": "sslh-lab",
"created-at": time.Now().Format("2006-01-02T15-04-05"),
},
}
server, err := c.CreateServer(req)
if err != nil {
return nil, fmt.Errorf("failed to create server: %w", err)
}
serverInfo := &ServerInfo{
ID: server.ID,
Name: server.Name,
Status: server.Status,
Region: location,
Type: serverType,
}
if server.PublicNet.IPv4.IP != "" {
serverInfo.PublicIP = server.PublicNet.IPv4.IP
} else {
if err := c.WaitForServerReady(server.ID, 10*time.Minute); err != nil {
return nil, fmt.Errorf("server created but failed to become ready: %w", err)
}
ip, err := c.GetServerIP(server.ID)
if err != nil {
return nil, fmt.Errorf("failed to get server IP: %w", err)
}
serverInfo.PublicIP = ip
}
return serverInfo, nil
}

View File

@@ -0,0 +1,107 @@
package hetzner
import (
"fmt"
)
type SSHKey struct {
ID int `json:"id"`
Name string `json:"name"`
Fingerprint string `json:"fingerprint"`
PublicKey string `json:"public_key"`
Labels map[string]string `json:"labels,omitempty"`
}
type SSHKeyResponse struct {
SSHKey SSHKey `json:"ssh_key"`
}
type SSHKeysResponse struct {
SSHKeys []SSHKey `json:"ssh_keys"`
}
type CreateSSHKeyRequest struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
Labels map[string]string `json:"labels,omitempty"`
}
func (c *Client) CreateSSHKey(name, publicKey string) (*SSHKey, error) {
req := CreateSSHKeyRequest{
Name: name,
PublicKey: publicKey,
Labels: map[string]string{
"managed-by": "sslh-lab",
},
}
var resp SSHKeyResponse
httpResp, err := c.resty.R().
SetBody(req).
SetResult(&resp).
Post("/ssh_keys")
if err != nil {
return nil, fmt.Errorf("failed to create SSH key: %w", err)
}
if httpResp.IsError() {
return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body()))
}
return &resp.SSHKey, nil
}
func (c *Client) GetSSHKeyByName(name string) (*SSHKey, error) {
var resp SSHKeysResponse
httpResp, err := c.resty.R().
SetResult(&resp).
Get("/ssh_keys")
if err != nil {
return nil, fmt.Errorf("failed to list SSH keys: %w", err)
}
if httpResp.IsError() {
return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body()))
}
for _, key := range resp.SSHKeys {
if key.Name == name {
return &key, nil
}
}
return nil, fmt.Errorf("SSH key with name '%s' not found", name)
}
func (c *Client) GetOrCreateSSHKey(name, publicKey string) (int, error) {
existingKey, err := c.GetSSHKeyByName(name)
if err == nil {
return existingKey.ID, nil
}
newKey, err := c.CreateSSHKey(name, publicKey)
if err != nil {
return 0, fmt.Errorf("failed to create SSH key: %w", err)
}
return newKey.ID, nil
}
func (c *Client) DeleteSSHKey(keyID int) error {
httpResp, err := c.resty.R().
Delete(fmt.Sprintf("/ssh_keys/%d", keyID))
if err != nil {
return fmt.Errorf("failed to delete SSH key: %w", err)
}
if httpResp.IsError() {
return fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body()))
}
return nil
}

View File

@@ -0,0 +1,216 @@
package letsencrypt
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"time"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
)
type Client struct {
user *User
client *lego.Client
certDir string
email string
dnsProvider DNSProvider
}
type User struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *User) GetEmail() string {
return u.Email
}
func (u *User) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *User) GetPrivateKey() crypto.PrivateKey {
return u.key
}
type DNSProvider interface {
Present(domain, token, keyAuth string) error
CleanUp(domain, token, keyAuth string) error
}
type CertificateInfo struct {
CertificatePath string
PrivateKeyPath string
FullChainPath string
Domain string
ExpiresAt time.Time
}
func NewClient(email, certDir string, dnsProvider DNSProvider) (*Client, error) {
if err := os.MkdirAll(certDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create cert directory: %w", err)
}
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate private key: %w", err)
}
user := &User{
Email: email,
key: privateKey,
}
config := lego.NewConfig(user)
config.CADirURL = lego.LEDirectoryProduction
config.Certificate.KeyType = certcrypto.RSA2048
config.HTTPClient.Timeout = 30 * time.Second
client, err := lego.NewClient(config)
if err != nil {
return nil, fmt.Errorf("failed to create lego client: %w", err)
}
provider := &namecheapDNSProvider{dnsProvider: dnsProvider}
client.Challenge.SetDNS01Provider(provider)
accountPath := filepath.Join(certDir, "account.json")
var reg *registration.Resource
if _, err := os.Stat(accountPath); err == nil {
accountData, err := os.ReadFile(accountPath)
if err == nil {
if err := json.Unmarshal(accountData, &reg); err == nil && reg != nil {
user.Registration = reg
}
}
}
if user.Registration == nil {
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, fmt.Errorf("failed to register with Let's Encrypt: %w", err)
}
user.Registration = reg
accountData, _ := json.Marshal(reg)
os.WriteFile(accountPath, accountData, 0600)
}
return &Client{
user: user,
client: client,
certDir: certDir,
email: email,
dnsProvider: dnsProvider,
}, nil
}
type namecheapDNSProvider struct {
dnsProvider DNSProvider
}
func (p *namecheapDNSProvider) Present(domain, token, keyAuth string) error {
return p.dnsProvider.Present(domain, token, keyAuth)
}
func (p *namecheapDNSProvider) CleanUp(domain, token, keyAuth string) error {
return p.dnsProvider.CleanUp(domain, token, keyAuth)
}
func (c *Client) ObtainCertificate(domain string, sanDomains []string) (*CertificateInfo, error) {
domains := append([]string{domain}, sanDomains...)
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
}
certificates, err := c.client.Certificate.Obtain(request)
if err != nil {
return nil, fmt.Errorf("failed to obtain certificate: %w", err)
}
certPath := filepath.Join(c.certDir, fmt.Sprintf("%s.crt", domain))
keyPath := filepath.Join(c.certDir, fmt.Sprintf("%s.key", domain))
fullChainPath := filepath.Join(c.certDir, fmt.Sprintf("%s-fullchain.crt", domain))
if err := os.WriteFile(certPath, certificates.Certificate, 0644); err != nil {
return nil, fmt.Errorf("failed to write certificate: %w", err)
}
if err := os.WriteFile(keyPath, certificates.PrivateKey, 0600); err != nil {
return nil, fmt.Errorf("failed to write private key: %w", err)
}
fullChain := append(certificates.Certificate, certificates.IssuerCertificate...)
if err := os.WriteFile(fullChainPath, fullChain, 0644); err != nil {
return nil, fmt.Errorf("failed to write full chain: %w", err)
}
block, _ := pem.Decode(certificates.Certificate)
if block == nil {
return nil, fmt.Errorf("failed to decode certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return &CertificateInfo{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
FullChainPath: fullChainPath,
Domain: domain,
ExpiresAt: cert.NotAfter,
}, nil
}
func (c *Client) RevokeCertificate(certPath string) error {
certBytes, err := os.ReadFile(certPath)
if err != nil {
return fmt.Errorf("failed to read certificate: %w", err)
}
reason := uint(4)
return c.client.Certificate.RevokeWithReason(certBytes, &reason)
}
func GetCertificateInfo(certPath string) (*CertificateInfo, error) {
certBytes, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to read certificate: %w", err)
}
block, _ := pem.Decode(certBytes)
if block == nil {
return nil, fmt.Errorf("failed to decode certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
keyPath := certPath[:len(certPath)-4] + ".key"
fullChainPath := certPath[:len(certPath)-4] + "-fullchain.crt"
return &CertificateInfo{
CertificatePath: certPath,
PrivateKeyPath: keyPath,
FullChainPath: fullChainPath,
Domain: cert.Subject.CommonName,
ExpiresAt: cert.NotAfter,
}, nil
}

View File

@@ -0,0 +1,94 @@
package letsencrypt
import (
"fmt"
"time"
"github.com/go-acme/lego/v4/challenge/dns01"
"sslh-multiplex-lab/internal/providers/namecheap"
)
type NamecheapDNSProvider struct {
namecheapClient *namecheap.Client
domain string
txtRecords map[string]string
}
func NewNamecheapDNSProvider(namecheapClient *namecheap.Client, domain string) *NamecheapDNSProvider {
return &NamecheapDNSProvider{
namecheapClient: namecheapClient,
domain: domain,
txtRecords: make(map[string]string),
}
}
func (p *NamecheapDNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
subdomain := extractSubdomain(fqdn, p.domain)
if subdomain == "" {
return fmt.Errorf("failed to extract subdomain from %s for domain %s", fqdn, p.domain)
}
p.txtRecords[subdomain] = value
_, err := p.namecheapClient.CreateOrUpdateDNSRecord(p.domain, subdomain, "TXT", value, 300)
if err != nil {
return fmt.Errorf("failed to create TXT record for %s: %w", subdomain, err)
}
time.Sleep(10 * time.Second)
return nil
}
func (p *NamecheapDNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
subdomain := extractSubdomain(fqdn, p.domain)
if subdomain == "" {
return nil
}
records, err := p.namecheapClient.ListDNSRecords(p.domain)
if err != nil {
return fmt.Errorf("failed to list DNS records: %w", err)
}
for _, record := range records {
if record.Name == subdomain && record.Type == "TXT" {
if err := p.namecheapClient.DeleteDNSRecord(p.domain, record.ID); err != nil {
return fmt.Errorf("failed to delete TXT record for %s: %w", subdomain, err)
}
}
}
delete(p.txtRecords, subdomain)
return nil
}
func extractSubdomain(fqdn, domain string) string {
if len(fqdn) <= len(domain) {
return ""
}
suffix := "." + domain
if !endsWith(fqdn, suffix) {
return ""
}
subdomain := fqdn[:len(fqdn)-len(suffix)]
if subdomain == "_acme-challenge" {
return "_acme-challenge"
}
if len(subdomain) > len("_acme-challenge.") && subdomain[:len("_acme-challenge.")] == "_acme-challenge." {
return subdomain
}
return ""
}
func endsWith(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

View File

@@ -0,0 +1,567 @@
package namecheap
import (
"encoding/xml"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
const (
namecheapAPIBaseURL = "https://api.namecheap.com/xml.response"
)
type Client struct {
apiKey string
apiUser string
clientIP string
resty *resty.Client
}
type Domain struct {
Name string
Nameservers []string
IsUsingNamecheapDNS bool
}
type DNSRecord struct {
ID string
Type string
Name string
Address string
TTL int
MXPref int
}
type DomainListResponse struct {
Domains []Domain
}
type DNSHostListResponse struct {
Records []DNSRecord
}
func NewClient(apiKey, apiUser, clientIP string) *Client {
client := resty.New()
client.SetBaseURL(namecheapAPIBaseURL)
client.SetTimeout(30 * time.Second)
return &Client{
apiKey: apiKey,
apiUser: apiUser,
clientIP: clientIP,
resty: client,
}
}
func (c *Client) buildQueryParams(command string, params map[string]string) url.Values {
query := url.Values{}
query.Set("ApiUser", c.apiUser)
query.Set("ApiKey", c.apiKey)
query.Set("UserName", c.apiUser)
query.Set("ClientIp", c.clientIP)
query.Set("Command", command)
for k, v := range params {
query.Set(k, v)
}
return query
}
func (c *Client) ListDomains() ([]Domain, error) {
query := c.buildQueryParams("namecheap.domains.getList", map[string]string{})
resp, err := c.resty.R().
SetQueryParamsFromValues(query).
Get("")
if err != nil {
return nil, fmt.Errorf("failed to list domains: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("API error: %s - %s", resp.Status(), string(resp.Body()))
}
var domains []Domain
if err := parseDomainListResponse(resp.Body(), &domains); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return domains, nil
}
func (c *Client) ListDNSRecords(domain string) ([]DNSRecord, error) {
query := c.buildQueryParams("namecheap.domains.dns.getHosts", map[string]string{
"SLD": extractSLD(domain),
"TLD": extractTLD(domain),
})
resp, err := c.resty.R().
SetQueryParamsFromValues(query).
Get("")
if err != nil {
return nil, fmt.Errorf("failed to list DNS records: %w", err)
}
if resp.IsError() {
bodyStr := string(resp.Body())
if strings.Contains(bodyStr, "2030288") || strings.Contains(bodyStr, "not using proper DNS servers") {
return nil, fmt.Errorf("domain %s is not using Namecheap DNS servers. The domain must use Namecheap's BasicDNS, PremiumDNS, or FreeDNS nameservers to manage DNS records via API. Please change the nameservers in your Namecheap account (Domain List > Manage > Nameservers) to 'Namecheap BasicDNS' or 'Namecheap PremiumDNS'", domain)
}
return nil, fmt.Errorf("API error: %s - %s", resp.Status(), bodyStr)
}
var records []DNSRecord
if err := parseDNSHostListResponse(resp.Body(), &records); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return records, nil
}
func (c *Client) CreateOrUpdateDNSRecord(domain, subdomain, recordType, value string, ttl int) (created bool, err error) {
sld := extractSLD(domain)
tld := extractTLD(domain)
records, err := c.ListDNSRecords(domain)
if err != nil {
return false, fmt.Errorf("failed to get existing records: %w", err)
}
recordExists := false
var updatedRecords []DNSRecord
for _, record := range records {
if record.Name == subdomain && record.Type == recordType {
recordExists = true
updatedRecords = append(updatedRecords, DNSRecord{
ID: record.ID,
Type: recordType,
Name: subdomain,
Address: value,
TTL: ttl,
MXPref: record.MXPref,
})
} else {
updatedRecords = append(updatedRecords, record)
}
}
if !recordExists {
updatedRecords = append(updatedRecords, DNSRecord{
ID: fmt.Sprintf("%d", len(records)+1),
Type: recordType,
Name: subdomain,
Address: value,
TTL: ttl,
MXPref: 10,
})
}
query := c.buildQueryParams("namecheap.domains.dns.setHosts", map[string]string{
"SLD": sld,
"TLD": tld,
})
for i, record := range updatedRecords {
idx := i + 1
query.Set(fmt.Sprintf("HostName%d", idx), record.Name)
query.Set(fmt.Sprintf("RecordType%d", idx), record.Type)
query.Set(fmt.Sprintf("Address%d", idx), record.Address)
query.Set(fmt.Sprintf("TTL%d", idx), fmt.Sprintf("%d", record.TTL))
if record.Type == "MX" {
query.Set(fmt.Sprintf("MXPref%d", idx), fmt.Sprintf("%d", record.MXPref))
}
}
resp, err := c.resty.R().
SetQueryParamsFromValues(query).
Get("")
if err != nil {
return false, fmt.Errorf("failed to set DNS record: %w", err)
}
if resp.IsError() {
return false, fmt.Errorf("API error: %s - %s", resp.Status(), string(resp.Body()))
}
return !recordExists, nil
}
func (c *Client) CreateDNSRecord(domain, subdomain, recordType, value string, ttl int) error {
_, err := c.CreateOrUpdateDNSRecord(domain, subdomain, recordType, value, ttl)
return err
}
func (c *Client) DeleteDNSRecord(domain, recordID string) error {
records, err := c.ListDNSRecords(domain)
if err != nil {
return fmt.Errorf("failed to get existing records: %w", err)
}
var filtered []DNSRecord
for _, record := range records {
if record.ID != recordID {
filtered = append(filtered, record)
}
}
sld := extractSLD(domain)
tld := extractTLD(domain)
query := c.buildQueryParams("namecheap.domains.dns.setHosts", map[string]string{
"SLD": sld,
"TLD": tld,
})
for i, record := range filtered {
query.Set(fmt.Sprintf("HostName%d", i+1), record.Name)
query.Set(fmt.Sprintf("RecordType%d", i+1), record.Type)
query.Set(fmt.Sprintf("Address%d", i+1), record.Address)
query.Set(fmt.Sprintf("TTL%d", i+1), fmt.Sprintf("%d", record.TTL))
}
resp, err := c.resty.R().
SetQueryParamsFromValues(query).
Get("")
if err != nil {
return fmt.Errorf("failed to delete DNS record: %w", err)
}
if resp.IsError() {
return fmt.Errorf("API error: %s - %s", resp.Status(), string(resp.Body()))
}
return nil
}
func (c *Client) CheckExistingRecords(domain string) (bool, []DNSRecord, error) {
records, err := c.ListDNSRecords(domain)
if err != nil {
return false, nil, err
}
return len(records) > 0, records, nil
}
func (c *Client) GetDomainInfo(domain string) (*Domain, error) {
query := c.buildQueryParams("namecheap.domains.getInfo", map[string]string{
"DomainName": domain,
})
resp, err := c.resty.R().
SetQueryParamsFromValues(query).
Get("")
if err != nil {
return nil, fmt.Errorf("failed to get domain info: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("API error: %s - %s", resp.Status(), string(resp.Body()))
}
var domainInfo Domain
if err := parseDomainInfoResponse(resp.Body(), &domainInfo); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &domainInfo, nil
}
func (c *Client) IsDomainUsingNamecheapDNS(domain string) (bool, error) {
domainInfo, err := c.GetDomainInfo(domain)
if err != nil {
return false, err
}
return domainInfo.IsUsingNamecheapDNS, nil
}
func (c *Client) SetDomainToNamecheapDNS(domain string) error {
sld := extractSLD(domain)
tld := extractTLD(domain)
if sld == "" || tld == "" {
return fmt.Errorf("invalid domain format: %s (could not extract SLD/TLD)", domain)
}
query := c.buildQueryParams("namecheap.domains.dns.setDefault", map[string]string{
"SLD": sld,
"TLD": tld,
})
resp, err := c.resty.R().
SetQueryParamsFromValues(query).
Get("")
if err != nil {
return fmt.Errorf("failed to set domain to Namecheap DNS: %w", err)
}
bodyStr := string(resp.Body())
if resp.IsError() {
return fmt.Errorf("API error: %s - %s", resp.Status(), bodyStr)
}
var result domainDNSSetDefaultResult
if err := parseDNSSetDefaultResponse(resp.Body(), &result); err != nil {
return fmt.Errorf("failed to parse response: %w\nAPI response: %s", err, bodyStr)
}
if !result.Updated {
return fmt.Errorf("API returned Updated=false for domain %s\nAPI response: %s", domain, bodyStr)
}
return nil
}
func extractSLD(domain string) string {
parts := splitDomain(domain)
if len(parts) >= 2 {
return parts[len(parts)-2]
}
return ""
}
func extractTLD(domain string) string {
parts := splitDomain(domain)
if len(parts) >= 1 {
return parts[len(parts)-1]
}
return ""
}
func splitDomain(domain string) []string {
var parts []string
start := 0
for i, char := range domain {
if char == '.' {
if i > start {
parts = append(parts, domain[start:i])
}
start = i + 1
}
}
if start < len(domain) {
parts = append(parts, domain[start:])
}
return parts
}
type apiResponse struct {
XMLName xml.Name `xml:"ApiResponse"`
Status string `xml:"Status,attr"`
Errors []apiError `xml:"Errors>Error"`
CommandResponse commandResponse `xml:"CommandResponse"`
}
type apiError struct {
Number string `xml:"Number,attr"`
Text string `xml:",chardata"`
}
type commandResponse struct {
DomainGetListResult domainGetListResult `xml:"DomainGetListResult"`
DomainDNSSetHostsResult domainDNSSetHostsResult `xml:"DomainDNSSetHostsResult"`
DomainDNSGetHostsResult domainDNSGetHostsResult `xml:"DomainDNSGetHostsResult"`
DomainGetInfoResult domainGetInfoResult `xml:"DomainGetInfoResult"`
DomainDNSSetDefaultResult domainDNSSetDefaultResult `xml:"DomainDNSSetDefaultResult"`
}
type domainGetListResult struct {
Domains []domainXML `xml:"Domain"`
}
type domainXML struct {
Name string `xml:"Name"`
}
type domainGetInfoResult struct {
DomainName string `xml:"DomainName,attr"`
IsUsingNamecheapDNS bool `xml:"IsUsingNamecheapDNS,attr"`
Nameservers []nameserverXML `xml:"Nameservers>Nameserver"`
}
type nameserverXML struct {
Name string `xml:",chardata"`
}
type domainDNSSetDefaultResult struct {
Domain string `xml:"Domain,attr"`
Updated bool `xml:"Updated,attr"`
}
type domainDNSGetHostsResult struct {
Hosts []hostXML `xml:"host"`
}
type domainDNSSetHostsResult struct {
IsSuccess bool `xml:"IsSuccess,attr"`
}
type hostXML struct {
Name string `xml:"Name,attr"`
Type string `xml:"Type,attr"`
Address string `xml:"Address,attr"`
TTL string `xml:"TTL,attr"`
MXPref string `xml:"MXPref,attr"`
}
func parseDomainListResponse(body []byte, domains *[]Domain) error {
var resp apiResponse
if err := xml.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to unmarshal XML: %w", err)
}
if resp.Status != "OK" {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
return fmt.Errorf("API returned error status: %s", errorMsgs)
}
if len(resp.Errors) > 0 {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
return fmt.Errorf("API errors in response: %s", errorMsgs)
}
*domains = make([]Domain, 0, len(resp.CommandResponse.DomainGetListResult.Domains))
for _, domainXML := range resp.CommandResponse.DomainGetListResult.Domains {
*domains = append(*domains, Domain{
Name: domainXML.Name,
})
}
return nil
}
func parseDNSHostListResponse(body []byte, records *[]DNSRecord) error {
var resp apiResponse
if err := xml.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to unmarshal XML: %w", err)
}
if resp.Status != "OK" {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
return fmt.Errorf("API returned error status: %s", errorMsgs)
}
if len(resp.Errors) > 0 {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
return fmt.Errorf("API errors in response: %s", errorMsgs)
}
*records = make([]DNSRecord, 0, len(resp.CommandResponse.DomainDNSGetHostsResult.Hosts))
for i, hostXML := range resp.CommandResponse.DomainDNSGetHostsResult.Hosts {
ttl := 1799
if hostXML.TTL != "" {
if parsedTTL, err := strconv.Atoi(hostXML.TTL); err == nil {
ttl = parsedTTL
}
}
mxPref := 10
if hostXML.MXPref != "" {
if parsedMXPref, err := strconv.Atoi(hostXML.MXPref); err == nil {
mxPref = parsedMXPref
}
}
*records = append(*records, DNSRecord{
ID: fmt.Sprintf("%d", i+1),
Type: hostXML.Type,
Name: hostXML.Name,
Address: hostXML.Address,
TTL: ttl,
MXPref: mxPref,
})
}
return nil
}
func parseDomainInfoResponse(body []byte, domain *Domain) error {
var resp apiResponse
if err := xml.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to unmarshal XML: %w", err)
}
if resp.Status != "OK" {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
return fmt.Errorf("API returned error status: %s", errorMsgs)
}
if len(resp.Errors) > 0 {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
return fmt.Errorf("API errors in response: %s", errorMsgs)
}
result := resp.CommandResponse.DomainGetInfoResult
domain.Name = result.DomainName
domain.IsUsingNamecheapDNS = result.IsUsingNamecheapDNS
domain.Nameservers = make([]string, 0, len(result.Nameservers))
for _, ns := range result.Nameservers {
domain.Nameservers = append(domain.Nameservers, ns.Name)
}
return nil
}
func parseDNSSetDefaultResponse(body []byte, result *domainDNSSetDefaultResult) error {
var resp apiResponse
if err := xml.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to unmarshal XML: %w\nResponse body: %s", err, string(body))
}
if resp.Status != "OK" {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
if len(errorMsgs) == 0 {
errorMsgs = append(errorMsgs, "unknown error (no error details in response)")
}
return fmt.Errorf("API returned error status: %s", errorMsgs)
}
if len(resp.Errors) > 0 {
var errorMsgs []string
for _, err := range resp.Errors {
errorMsgs = append(errorMsgs, fmt.Sprintf("[%s] %s", err.Number, err.Text))
}
return fmt.Errorf("API errors in response: %s", errorMsgs)
}
if resp.CommandResponse.DomainDNSSetDefaultResult.Domain == "" {
return fmt.Errorf("API response missing DomainDNSSetDefaultResult\nResponse body: %s", string(body))
}
*result = resp.CommandResponse.DomainDNSSetDefaultResult
return nil
}

View File

@@ -0,0 +1,34 @@
package namecheap
import (
"fmt"
"time"
)
func (c *Client) WaitForDNSPropagation(domain, subdomain, expectedIP string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
backoff := 5 * time.Second
maxBackoff := 80 * time.Second
for {
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for DNS propagation")
}
records, err := c.ListDNSRecords(domain)
if err != nil {
return fmt.Errorf("failed to check DNS records: %w", err)
}
for _, record := range records {
if record.Name == subdomain && record.Address == expectedIP {
return nil
}
}
time.Sleep(backoff)
if backoff < maxBackoff {
backoff *= 2
}
}
}