Skip to content

Commit 817828f

Browse files
authored
add: IONOS Hosting Services provider (#225)
1 parent f558d78 commit 817828f

File tree

5 files changed

+250
-12
lines changed

5 files changed

+250
-12
lines changed

README.md

+47-12
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,25 @@
4949
- [Strato](#strato)
5050
- [LoopiaSE](#loopiase)
5151
- [Infomaniak](#infomaniak)
52+
- [Hetzner](#hetzner)
5253
- [OVH](#ovh)
5354
- [Dynu](#dynu)
55+
- [IONOS](#ionos)
5456
- [Notifications](#notifications)
5557
- [Email](#email)
5658
- [Telegram](#telegram)
5759
- [Slack](#slack)
5860
- [Discord](#discord)
5961
- [Pushover](#pushover)
6062
- [Webhook](#webhook)
61-
- [HTTP GET Request](#webhook-with-http-get-reqeust)
62-
- [HTTP POST Request](#webhook-with-http-post-request)
63+
- [Webhook with HTTP GET reqeust](#webhook-with-http-get-reqeust)
64+
- [Webhook with HTTP POST request](#webhook-with-http-post-request)
6365
- [Miscellaneous topics](#miscellaneous-topics)
6466
- [IPv6 support](#ipv6-support)
6567
- [Network interface IP address](#network-interface-ip-address)
6668
- [SOCKS5 proxy support](#socks5-proxy-support)
6769
- [Display debug info](#display-debug-info)
70+
- [Multiple API URLs](#multiple-api-urls)
6871
- [Recommended APIs](#recommended-apis)
6972
- [Running GoDNS](#running-godns)
7073
- [Manually](#manually)
@@ -98,6 +101,7 @@
98101
| [Hetzner][hetzner] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
99102
| [OVH][ovh] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
100103
| [Dynu][dynu] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
104+
| [IONOS][ionos] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
101105

102106
[cloudflare]: https://cloudflare.com
103107
[google.domains]: https://domains.google
@@ -116,6 +120,7 @@
116120
[hetzner]: https://hetzner.com/
117121
[ovh]: https://www.ovh.com
118122
[dynu]: https://www.dynu.com/
123+
[ionos]: https://www.ionos.com/
119124

120125
Tip: You can follow this [issue](https://github.com/TimothyYe/godns/issues/76) to view the current status of DDNS for root domains.
121126

@@ -540,7 +545,7 @@ For Scaleway, you need to provide an API Secret Key as the `login_token` ([How t
540545

541546
<details>
542547
<summary>Example</summary>
543-
548+
544549
```json
545550
{
546551
"provider": "Scaleway",
@@ -559,6 +564,7 @@ For Scaleway, you need to provide an API Secret Key as the `login_token` ([How t
559564
"interval": 300
560565
}
561566
```
567+
562568
</details>
563569

564570
#### Linode
@@ -571,7 +577,7 @@ The GoDNS Linode handler currently uses a fixed TTL of 30 seconds for Linode DNS
571577

572578
<details>
573579
<summary>Example</summary>
574-
580+
575581
```json
576582
{
577583
"provider": "Linode",
@@ -590,6 +596,7 @@ The GoDNS Linode handler currently uses a fixed TTL of 30 seconds for Linode DNS
590596
"interval": 300
591597
}
592598
```
599+
593600
</details>
594601

595602
#### Strato
@@ -787,6 +794,34 @@ For Dynu, you need to configure the `password`, config 1 default domain & subdom
787794

788795
</details>
789796

797+
#### IONOS
798+
799+
This is for IONOS Hosting Services, **not** IONOS Cloud.
800+
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).
801+
You can find a full guide in the [IONOS API Documentation](https://developer.hosting.ionos.com/docs/getstarted).
802+
**Note**: The API-Key used by GoDNS must follow the form `publicprefix.secret` as described in the aforementioned documentation.
803+
804+
<details>
805+
<summary>Example</summary>
806+
807+
```yaml
808+
provider: IONOS
809+
login_token: publicprefix.secret
810+
domains:
811+
- domain_name: example.com
812+
sub_domains:
813+
- somesubdomain
814+
- anothersubdomain
815+
resolver: 1.1.1.1
816+
ip_urls:
817+
- https://api.ipify.org
818+
ip_type: IPv4
819+
interval: 300
820+
socks5_proxy: ""
821+
```
822+
823+
</details>
824+
790825
### Notifications
791826
792827
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
10181053

10191054
#### Recommended APIs
10201055

1021-
- https://api.ipify.org
1022-
- https://myip.biturl.top
1023-
- https://ip4.seeip.org
1024-
- https://ipecho.net/plain
1025-
- https://api-ipv4.ip.sb/ip
1056+
- <https://api.ipify.org>
1057+
- <https://myip.biturl.top>
1058+
- <https://ip4.seeip.org>
1059+
- <https://ipecho.net/plain>
1060+
- <https://api-ipv4.ip.sb/ip>
10261061

10271062
## Running GoDNS
10281063

@@ -1080,10 +1115,10 @@ Note: when the program stops, it will not be restarted.
10801115

10811116
Available docker registries:
10821117

1083-
- https://hub.docker.com/r/timothyye/godns
1084-
- https://github.com/TimothyYe/godns/pkgs/container/godns
1118+
- <https://hub.docker.com/r/timothyye/godns>
1119+
- <https://github.com/TimothyYe/godns/pkgs/container/godns>
10851120

1086-
Visit https://hub.docker.com/r/timothyye/godns to fetch the latest docker image.
1121+
Visit <https://hub.docker.com/r/timothyye/godns> to fetch the latest docker image.
10871122
With `/path/to/config.json` your local configuration file, run:
10881123

10891124
```bash

internal/provider/factory.go

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/TimothyYe/godns/internal/provider/he"
1515
"github.com/TimothyYe/godns/internal/provider/hetzner"
1616
"github.com/TimothyYe/godns/internal/provider/infomaniak"
17+
"github.com/TimothyYe/godns/internal/provider/ionos"
1718
"github.com/TimothyYe/godns/internal/provider/linode"
1819
"github.com/TimothyYe/godns/internal/provider/loopiase"
1920
"github.com/TimothyYe/godns/internal/provider/noip"
@@ -62,6 +63,8 @@ func GetProvider(conf *settings.Settings) (IDNSProvider, error) {
6263
provider = &ovh.DNSProvider{}
6364
case utils.DYNU:
6465
provider = &dynu.DNSProvider{}
66+
case utils.IONOS:
67+
provider = &ionos.DNSProvider{}
6568
default:
6669
return nil, fmt.Errorf("Unknown provider '%s'", conf.Provider)
6770
}
+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package ionos
2+
3+
// API Docs: https://developer.hosting.ionos.com/docs/dns
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
12+
"github.com/TimothyYe/godns/internal/settings"
13+
"github.com/TimothyYe/godns/internal/utils"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
const (
18+
BaseURL = "https://api.hosting.ionos.com/dns/v1/"
19+
)
20+
21+
// DNSProvider struct.
22+
type DNSProvider struct {
23+
configuration *settings.Settings
24+
client *http.Client
25+
}
26+
27+
// Init passes DNS settings and store it to the provider instance.
28+
func (provider *DNSProvider) Init(conf *settings.Settings) {
29+
provider.configuration = conf
30+
provider.client = utils.GetHTTPClient(provider.configuration)
31+
}
32+
33+
func (provider *DNSProvider) UpdateIP(domainName, subdomainName, ip string) error {
34+
zoneID, err := provider.getZoneID(domainName)
35+
if err != nil {
36+
return err
37+
}
38+
39+
recordID, currIP, err := provider.getRecord(zoneID, subdomainName+"."+domainName)
40+
if err != nil {
41+
return err
42+
}
43+
44+
if currIP == ip {
45+
return nil
46+
}
47+
48+
return provider.updateRecord(zoneID, recordID, subdomainName+"."+domainName, ip)
49+
}
50+
51+
func (provider *DNSProvider) getData(endpoint string, params map[string]string) ([]byte, error) {
52+
req, err := http.NewRequest(http.MethodGet, BaseURL+endpoint, nil)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
req.Header.Add("X-API-Key", provider.configuration.LoginToken)
58+
59+
if params != nil {
60+
q := req.URL.Query()
61+
for k, v := range params {
62+
q.Add(k, v)
63+
}
64+
req.URL.RawQuery = q.Encode()
65+
}
66+
67+
resp, err := provider.client.Do(req)
68+
if err != nil {
69+
return nil, err
70+
}
71+
if resp.StatusCode != http.StatusOK {
72+
return nil, fmt.Errorf("failed to get data from %s, status code: %s", BaseURL+endpoint, resp.Status)
73+
}
74+
defer resp.Body.Close()
75+
76+
return io.ReadAll(resp.Body)
77+
78+
}
79+
80+
func (provider *DNSProvider) putData(endpoint string, params map[string]any) error {
81+
82+
var body []byte
83+
var err error
84+
if params != nil {
85+
body, err = json.Marshal(params)
86+
if err != nil {
87+
return err
88+
}
89+
}
90+
91+
req, err := http.NewRequest(http.MethodPut, BaseURL+endpoint, bytes.NewReader(body))
92+
if err != nil {
93+
return err
94+
}
95+
96+
req.Header.Add("Content-Type", "application/json")
97+
req.Header.Add("X-API-Key", provider.configuration.LoginToken)
98+
99+
resp, err := provider.client.Do(req)
100+
if err != nil {
101+
return err
102+
}
103+
if resp.StatusCode != http.StatusOK {
104+
return fmt.Errorf("failed to PUT %s, status: %s", endpoint, resp.Status)
105+
}
106+
defer resp.Body.Close()
107+
108+
return nil
109+
}
110+
111+
type zoneResponse struct {
112+
ID string `json:"id"`
113+
Name string `json:"name"`
114+
Type string `json:"type"`
115+
}
116+
117+
func (provider *DNSProvider) getZoneID(domainName string) (string, error) {
118+
119+
body, err := provider.getData("zones", nil)
120+
if err != nil {
121+
return "", err
122+
}
123+
124+
var zones []zoneResponse
125+
err = json.Unmarshal(body, &zones)
126+
if err != nil {
127+
return "", err
128+
}
129+
130+
for _, zone := range zones {
131+
if zone.Name == domainName {
132+
return zone.ID, nil
133+
}
134+
}
135+
136+
return "", fmt.Errorf("zone %s not found", domainName)
137+
}
138+
139+
type recordResponse struct {
140+
ID string `json:"id"`
141+
Name string `json:"name"`
142+
RootName string `json:"rootName"`
143+
Type string `json:"type"`
144+
Content string `json:"content"`
145+
TTL int `json:"ttl"`
146+
Prio int `json:"prio"`
147+
Disabled bool `json:"disabled"`
148+
}
149+
150+
type recordListResponse struct {
151+
zoneResponse
152+
Records []recordResponse `json:"records"`
153+
}
154+
155+
func (provider *DNSProvider) getRecord(zoneID, recordName string) (id string, ip string, err error) {
156+
157+
ipType := utils.IPTypeA
158+
if provider.configuration.IPType == utils.IPV6 || provider.configuration.IPType == utils.IPTypeAAAA {
159+
ipType = utils.IPTypeAAAA
160+
}
161+
162+
body, err := provider.getData(fmt.Sprintf("zones/%s", zoneID),
163+
map[string]string{
164+
"recordName": recordName,
165+
"recordType": ipType,
166+
})
167+
if err != nil {
168+
return "", "", err
169+
}
170+
171+
var rlp recordListResponse
172+
err = json.Unmarshal(body, &rlp)
173+
if err != nil {
174+
return "", "", err
175+
}
176+
177+
if len(rlp.Records) > 0 {
178+
return rlp.Records[0].ID, rlp.Records[0].Content, nil
179+
}
180+
181+
return "", "", fmt.Errorf("record %s not found", recordName)
182+
}
183+
184+
func (provider *DNSProvider) updateRecord(zoneID, recordID, recordName, ip string) error {
185+
186+
err := provider.putData(fmt.Sprintf("zones/%s/records/%s", zoneID, recordID), map[string]any{"content": ip})
187+
if err != nil {
188+
return fmt.Errorf("failed to update record %s: %w", recordName, err)
189+
}
190+
191+
logrus.Infof("Updated record %s to %s", recordName, ip)
192+
193+
return nil
194+
}

internal/utils/constants.go

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const (
3737
HETZNER = "Hetzner"
3838
// OVH for OVH.
3939
OVH = "OVH"
40+
// IONOS for IONOS.
41+
IONOS = "IONOS"
4042
// IPV4 for IPV4 mode.
4143
IPV4 = "IPV4"
4244
// IPV6 for IPV6 mode.

internal/utils/settings.go

+4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ func CheckSettings(config *settings.Settings) error {
8383
if config.LoginToken == "" {
8484
return errors.New("login token cannot be empty")
8585
}
86+
case IONOS:
87+
if config.LoginToken == "" {
88+
return errors.New("login token cannot be empty")
89+
}
8690
case OVH:
8791
if config.AppKey == "" {
8892
return errors.New("app key cannot be empty")

0 commit comments

Comments
 (0)