package ionosDnsProvider import ( "context" "errors" "net" "net/http" "realdnydns/model/common" "realdnydns/pkg/dnsProvider" "sync" ionosDnsClient "gitea.t000-n.de/t.behrendt/ionosDnsClient" ) type IONOS struct { client *ionosDnsClient.ClientWithResponses zoneIdMap map[string]string defaultTtl int defaultPrio int zoneIdMapMutex sync.Mutex } type IONOSConfig struct { APIKey string `yaml:"api_key"` BaseURL string `yaml:"base_url"` DefaultTTL *int `yaml:"default_ttl,omitempty"` DefaultPrio *int `yaml:"default_prio,omitempty"` } func NewIonos(config *IONOSConfig) (dnsProvider.DNSProvider, error) { if config.APIKey == "" { return nil, errors.New("api_key is required") } if config.BaseURL == "" { return nil, errors.New("base_url is required") } options := []ionosDnsClient.ClientOption{ ionosDnsClient.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { req.Header.Set("X-API-Key", config.APIKey) return nil }), } client, err := ionosDnsClient.NewClientWithResponses(config.BaseURL, options...) if err != nil { return nil, err } // Set default values for TTL and Prio if not provided defaultTtl := 300 if config.DefaultTTL != nil { defaultTtl = *config.DefaultTTL } defaultPrio := 0 if config.DefaultPrio != nil { defaultPrio = *config.DefaultPrio } return &IONOS{ client: client, zoneIdMap: make(map[string]string), defaultTtl: defaultTtl, defaultPrio: defaultPrio, }, nil } func (i *IONOS) UpdateRecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) { zoneId, err := i.getZoneIdForTld(tld) if err != nil { return nil, err } domain := assembleTldSubdomainToDomain(tld, subdomain) recordId, err := i.getRecordIdForDomain(zoneId, domain) if err != nil { return nil, err } ipString := ip.String() res, err := i.client.UpdateRecordWithResponse(context.Background(), zoneId, recordId, ionosDnsClient.RecordUpdate{ Content: &ipString, Ttl: &ttl, Prio: &prio, Disabled: &disabled, }) if err != nil || res.StatusCode() != 200 { return nil, errors.New("failed to update record") } if res.JSON200 == nil { return nil, errors.New("record response is empty") } record := *res.JSON200 if record.Name == nil || record.Content == nil { return nil, errors.New("record is empty") } // Handle optional fields with defaults recordTtl := 0 if record.Ttl != nil { recordTtl = *record.Ttl } recordPrio := 0 if record.Prio != nil { recordPrio = *record.Prio } recordDisabled := false if record.Disabled != nil { recordDisabled = *record.Disabled } return &common.ARecord{ Domain: *record.Name, IP: *record.Content, TTL: recordTtl, Prio: recordPrio, Disabled: recordDisabled, }, nil } func (i *IONOS) getZoneIdForTld(tld string) (string, error) { i.zoneIdMapMutex.Lock() defer i.zoneIdMapMutex.Unlock() if zoneId, ok := i.zoneIdMap[tld]; ok { return zoneId, nil } res, err := i.client.GetZonesWithResponse(context.Background()) if err != nil { return "", err } else if res.StatusCode() != 200 { return "", errors.New("failed to get zones") } if res.JSON200 == nil { return "", errors.New("zones response is empty") } zones := *res.JSON200 for _, zone := range zones { if *zone.Name == tld { if zone.Id == nil || *zone.Id == "" { return "", errors.New("zone id is empty") } i.zoneIdMap[tld] = *zone.Id return *zone.Id, nil } } return "", errors.New("no zone found") } var recordTypeA = "A" func assembleTldSubdomainToDomain(tld string, subdomain string) string { if subdomain == "@" || subdomain == "" { return tld } return subdomain + "." + tld } func (i *IONOS) getRecordIdForDomain(zoneId string, domain string) (string, error) { res, err := i.client.GetZoneWithResponse(context.Background(), zoneId, &ionosDnsClient.GetZoneParams{ RecordName: &domain, RecordType: &recordTypeA, }) if err != nil || res.StatusCode() != 200 { return "", errors.New("failed to get zone") } if res.JSON200 == nil { return "", errors.New("zone response is empty") } zone := *res.JSON200 for _, record := range *zone.Records { if *record.Name == domain && *record.Type == ionosDnsClient.RecordTypes("A") { return *record.Id, nil } } return "", errors.New("no record found") } func (i *IONOS) GetRecord(tld string, subdomain string) (*common.ARecord, error) { zoneId, err := i.getZoneIdForTld(tld) if err != nil { return nil, err } domain := assembleTldSubdomainToDomain(tld, subdomain) recordId, err := i.getRecordIdForDomain(zoneId, domain) if err != nil { return nil, err } res, err := i.client.GetRecordWithResponse(context.Background(), zoneId, recordId) if err != nil || res.StatusCode() != 200 || res.JSON200 == nil { return nil, errors.New("failed to get record") } record := *res.JSON200 if record.Name == nil || *record.Name == "" || *record.Name != domain { return nil, errors.New("record name does not match or is empty") } if record.Content == nil || *record.Content == "" { return nil, errors.New("record content is empty") } needsUpdate := false ttl := i.defaultTtl if record.Ttl != nil { ttl = *record.Ttl } else { needsUpdate = true } prio := i.defaultPrio if record.Prio != nil { prio = *record.Prio } else { needsUpdate = true } disabled := false if record.Disabled != nil { disabled = *record.Disabled } else { needsUpdate = true } // realDynDns requires every field to be set, so we need to update the record if any of the fields are missing if needsUpdate { ip := net.ParseIP(*record.Content) if ip == nil { return nil, errors.New("invalid IP address in record content") } updatedRecord, err := i.UpdateRecord(tld, subdomain, ip, ttl, prio, disabled) if err != nil { return nil, err } return updatedRecord, nil } return &common.ARecord{ Domain: *record.Name, IP: *record.Content, TTL: ttl, Prio: prio, Disabled: disabled, }, nil }