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")