568 lines
14 KiB
Go
568 lines
14 KiB
Go
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
|
|
}
|