diff --git a/README.md b/README.md index a4d0c163..b206f77d 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,10 @@ - [Strato](#strato) - [LoopiaSE](#loopiase) - [Infomaniak](#infomaniak) + - [Hetzner](#hetzner) - [OVH](#ovh) - [Dynu](#dynu) + - [IONOS](#ionos) - [Notifications](#notifications) - [Email](#email) - [Telegram](#telegram) @@ -58,13 +60,14 @@ - [Discord](#discord) - [Pushover](#pushover) - [Webhook](#webhook) - - [HTTP GET Request](#webhook-with-http-get-reqeust) - - [HTTP POST Request](#webhook-with-http-post-request) + - [Webhook with HTTP GET reqeust](#webhook-with-http-get-reqeust) + - [Webhook with HTTP POST request](#webhook-with-http-post-request) - [Miscellaneous topics](#miscellaneous-topics) - [IPv6 support](#ipv6-support) - [Network interface IP address](#network-interface-ip-address) - [SOCKS5 proxy support](#socks5-proxy-support) - [Display debug info](#display-debug-info) + - [Multiple API URLs](#multiple-api-urls) - [Recommended APIs](#recommended-apis) - [Running GoDNS](#running-godns) - [Manually](#manually) @@ -98,6 +101,7 @@ | [Hetzner][hetzner] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | [OVH][ovh] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | | [Dynu][dynu] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | +| [IONOS][ionos] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | [cloudflare]: https://cloudflare.com [google.domains]: https://domains.google @@ -116,6 +120,7 @@ [hetzner]: https://hetzner.com/ [ovh]: https://www.ovh.com [dynu]: https://www.dynu.com/ +[ionos]: https://www.ionos.com/ Tip: You can follow this [issue](https://github.com/TimothyYe/godns/issues/76) to view the current status of DDNS for root domains. @@ -540,7 +545,7 @@ For Scaleway, you need to provide an API Secret Key as the `login_token` ([How t
Example - + ```json { "provider": "Scaleway", @@ -559,6 +564,7 @@ For Scaleway, you need to provide an API Secret Key as the `login_token` ([How t "interval": 300 } ``` +
#### Linode @@ -571,7 +577,7 @@ The GoDNS Linode handler currently uses a fixed TTL of 30 seconds for Linode DNS
Example - + ```json { "provider": "Linode", @@ -590,6 +596,7 @@ The GoDNS Linode handler currently uses a fixed TTL of 30 seconds for Linode DNS "interval": 300 } ``` +
#### Strato @@ -787,6 +794,34 @@ For Dynu, you need to configure the `password`, config 1 default domain & subdom +#### IONOS + +This is for IONOS Hosting Services, **not** IONOS Cloud. +You'll need to [sign up for API Access to Hosting Services](https://my.ionos.com/shop/product/ionos-api), then create an [API Key](https://developer.hosting.ionos.com/keys). +You can find a full guide in the [IONOS API Documentation](https://developer.hosting.ionos.com/docs/getstarted). +**Note**: The API-Key used by GoDNS must follow the form `publicprefix.secret` as described in the aforementioned documentation. + +
+Example + +```yaml +provider: IONOS +login_token: publicprefix.secret +domains: + - domain_name: example.com + sub_domains: + - somesubdomain + - anothersubdomain +resolver: 1.1.1.1 +ip_urls: + - https://api.ipify.org +ip_type: IPv4 +interval: 300 +socks5_proxy: "" +``` + +
+ ### Notifications GoDNS can send a notification each time the IP changes. @@ -1018,11 +1053,11 @@ GoDNS supports to fetch the public IP from multiple URLs via a simple round-robi #### Recommended APIs -- https://api.ipify.org -- https://myip.biturl.top -- https://ip4.seeip.org -- https://ipecho.net/plain -- https://api-ipv4.ip.sb/ip +- +- +- +- +- ## Running GoDNS @@ -1080,10 +1115,10 @@ Note: when the program stops, it will not be restarted. Available docker registries: -- https://hub.docker.com/r/timothyye/godns -- https://github.com/TimothyYe/godns/pkgs/container/godns +- +- -Visit https://hub.docker.com/r/timothyye/godns to fetch the latest docker image. +Visit to fetch the latest docker image. With `/path/to/config.json` your local configuration file, run: ```bash diff --git a/internal/provider/factory.go b/internal/provider/factory.go index e6df16c5..57bd99ab 100644 --- a/internal/provider/factory.go +++ b/internal/provider/factory.go @@ -14,6 +14,7 @@ import ( "github.com/TimothyYe/godns/internal/provider/he" "github.com/TimothyYe/godns/internal/provider/hetzner" "github.com/TimothyYe/godns/internal/provider/infomaniak" + "github.com/TimothyYe/godns/internal/provider/ionos" "github.com/TimothyYe/godns/internal/provider/linode" "github.com/TimothyYe/godns/internal/provider/loopiase" "github.com/TimothyYe/godns/internal/provider/noip" @@ -62,6 +63,8 @@ func GetProvider(conf *settings.Settings) (IDNSProvider, error) { provider = &ovh.DNSProvider{} case utils.DYNU: provider = &dynu.DNSProvider{} + case utils.IONOS: + provider = &ionos.DNSProvider{} default: return nil, fmt.Errorf("Unknown provider '%s'", conf.Provider) } diff --git a/internal/provider/ionos/ionos_handler.go b/internal/provider/ionos/ionos_handler.go new file mode 100644 index 00000000..22832f7f --- /dev/null +++ b/internal/provider/ionos/ionos_handler.go @@ -0,0 +1,194 @@ +package ionos + +// API Docs: https://developer.hosting.ionos.com/docs/dns + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/TimothyYe/godns/internal/settings" + "github.com/TimothyYe/godns/internal/utils" + "github.com/sirupsen/logrus" +) + +const ( + BaseURL = "https://api.hosting.ionos.com/dns/v1/" +) + +// DNSProvider struct. +type DNSProvider struct { + configuration *settings.Settings + client *http.Client +} + +// Init passes DNS settings and store it to the provider instance. +func (provider *DNSProvider) Init(conf *settings.Settings) { + provider.configuration = conf + provider.client = utils.GetHTTPClient(provider.configuration) +} + +func (provider *DNSProvider) UpdateIP(domainName, subdomainName, ip string) error { + zoneID, err := provider.getZoneID(domainName) + if err != nil { + return err + } + + recordID, currIP, err := provider.getRecord(zoneID, subdomainName+"."+domainName) + if err != nil { + return err + } + + if currIP == ip { + return nil + } + + return provider.updateRecord(zoneID, recordID, subdomainName+"."+domainName, ip) +} + +func (provider *DNSProvider) getData(endpoint string, params map[string]string) ([]byte, error) { + req, err := http.NewRequest(http.MethodGet, BaseURL+endpoint, nil) + if err != nil { + return nil, err + } + + req.Header.Add("X-API-Key", provider.configuration.LoginToken) + + if params != nil { + q := req.URL.Query() + for k, v := range params { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + resp, err := provider.client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get data from %s, status code: %s", BaseURL+endpoint, resp.Status) + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) + +} + +func (provider *DNSProvider) putData(endpoint string, params map[string]any) error { + + var body []byte + var err error + if params != nil { + body, err = json.Marshal(params) + if err != nil { + return err + } + } + + req, err := http.NewRequest(http.MethodPut, BaseURL+endpoint, bytes.NewReader(body)) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-API-Key", provider.configuration.LoginToken) + + resp, err := provider.client.Do(req) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to PUT %s, status: %s", endpoint, resp.Status) + } + defer resp.Body.Close() + + return nil +} + +type zoneResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +func (provider *DNSProvider) getZoneID(domainName string) (string, error) { + + body, err := provider.getData("zones", nil) + if err != nil { + return "", err + } + + var zones []zoneResponse + err = json.Unmarshal(body, &zones) + if err != nil { + return "", err + } + + for _, zone := range zones { + if zone.Name == domainName { + return zone.ID, nil + } + } + + return "", fmt.Errorf("zone %s not found", domainName) +} + +type recordResponse struct { + ID string `json:"id"` + Name string `json:"name"` + RootName string `json:"rootName"` + Type string `json:"type"` + Content string `json:"content"` + TTL int `json:"ttl"` + Prio int `json:"prio"` + Disabled bool `json:"disabled"` +} + +type recordListResponse struct { + zoneResponse + Records []recordResponse `json:"records"` +} + +func (provider *DNSProvider) getRecord(zoneID, recordName string) (id string, ip string, err error) { + + ipType := utils.IPTypeA + if provider.configuration.IPType == utils.IPV6 || provider.configuration.IPType == utils.IPTypeAAAA { + ipType = utils.IPTypeAAAA + } + + body, err := provider.getData(fmt.Sprintf("zones/%s", zoneID), + map[string]string{ + "recordName": recordName, + "recordType": ipType, + }) + if err != nil { + return "", "", err + } + + var rlp recordListResponse + err = json.Unmarshal(body, &rlp) + if err != nil { + return "", "", err + } + + if len(rlp.Records) > 0 { + return rlp.Records[0].ID, rlp.Records[0].Content, nil + } + + return "", "", fmt.Errorf("record %s not found", recordName) +} + +func (provider *DNSProvider) updateRecord(zoneID, recordID, recordName, ip string) error { + + err := provider.putData(fmt.Sprintf("zones/%s/records/%s", zoneID, recordID), map[string]any{"content": ip}) + if err != nil { + return fmt.Errorf("failed to update record %s: %w", recordName, err) + } + + logrus.Infof("Updated record %s to %s", recordName, ip) + + return nil +} diff --git a/internal/utils/constants.go b/internal/utils/constants.go index e4e498a8..f1525d50 100644 --- a/internal/utils/constants.go +++ b/internal/utils/constants.go @@ -37,6 +37,8 @@ const ( HETZNER = "Hetzner" // OVH for OVH. OVH = "OVH" + // IONOS for IONOS. + IONOS = "IONOS" // IPV4 for IPV4 mode. IPV4 = "IPV4" // IPV6 for IPV6 mode. diff --git a/internal/utils/settings.go b/internal/utils/settings.go index a9ec8ea5..a73ca831 100644 --- a/internal/utils/settings.go +++ b/internal/utils/settings.go @@ -83,6 +83,10 @@ func CheckSettings(config *settings.Settings) error { if config.LoginToken == "" { return errors.New("login token cannot be empty") } + case IONOS: + if config.LoginToken == "" { + return errors.New("login token cannot be empty") + } case OVH: if config.AppKey == "" { return errors.New("app key cannot be empty")