Skip to content

Commit

Permalink
add: IONOS Hosting Services provider (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
iwilltry42 authored Jan 5, 2024
1 parent f558d78 commit 817828f
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 12 deletions.
59 changes: 47 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,25 @@
- [Strato](#strato)
- [LoopiaSE](#loopiase)
- [Infomaniak](#infomaniak)
- [Hetzner](#hetzner)
- [OVH](#ovh)
- [Dynu](#dynu)
- [IONOS](#ionos)
- [Notifications](#notifications)
- [Email](#email)
- [Telegram](#telegram)
- [Slack](#slack)
- [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)
Expand Down Expand Up @@ -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
Expand All @@ -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.

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

<details>
<summary>Example</summary>

```json
{
"provider": "Scaleway",
Expand All @@ -559,6 +564,7 @@ For Scaleway, you need to provide an API Secret Key as the `login_token` ([How t
"interval": 300
}
```

</details>

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

<details>
<summary>Example</summary>

```json
{
"provider": "Linode",
Expand All @@ -590,6 +596,7 @@ The GoDNS Linode handler currently uses a fixed TTL of 30 seconds for Linode DNS
"interval": 300
}
```

</details>

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

</details>

#### 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.

<details>
<summary>Example</summary>

```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: ""
```
</details>
### Notifications
GoDNS can send a notification each time the IP changes.
Expand Down Expand Up @@ -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
- <https://api.ipify.org>
- <https://myip.biturl.top>
- <https://ip4.seeip.org>
- <https://ipecho.net/plain>
- <https://api-ipv4.ip.sb/ip>

## Running GoDNS

Expand Down Expand Up @@ -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
- <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 <https://hub.docker.com/r/timothyye/godns> to fetch the latest docker image.
With `/path/to/config.json` your local configuration file, run:

```bash
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
194 changes: 194 additions & 0 deletions internal/provider/ionos/ionos_handler.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions internal/utils/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions internal/utils/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 817828f

Please sign in to comment.