Skip to content

Commit

Permalink
feat(provider): add Ionos provider
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed Jan 15, 2024
1 parent 7ed63a0 commit ff5767a
Show file tree
Hide file tree
Showing 10 changed files with 485 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
- Hetzner
- Infomaniak
- INWX
- Ionos
- Linode
- LuaDNS
- Name.com
Expand Down Expand Up @@ -193,6 +194,7 @@ Check the documentation for your DNS provider:
- [He.net](https://github.com/qdm12/ddns-updater/blob/master/docs/he.net.md)
- [Infomaniak](https://github.com/qdm12/ddns-updater/blob/master/docs/infomaniak.md)
- [INWX](https://github.com/qdm12/ddns-updater/blob/master/docs/inwx.md)
- [Ionos](https://github.com/qdm12/ddns-updater/blob/master/docs/ionos.md)
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
- [Name.com](https://github.com/qdm12/ddns-updater/blob/master/docs/name.com.md)
Expand Down
29 changes: 29 additions & 0 deletions docs/ionos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Ionos

## Configuration

### Example

```json
{
"settings": [
{
"provider": "ionos",
"domain": "domain.com",
"host": "@",
"api_key": "api_key",
"ip_version": "ipv4"
}
]
}
```

### Compulsory parameters

- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"api_key"` is your API key, obtained from [creating an API key](https://www.ionos.com/help/domains/configuring-your-ip-address/set-up-dynamic-dns-with-company-name/#c181598)

### Optional parameters

- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
2 changes: 2 additions & 0 deletions internal/provider/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
Hetzner models.Provider = "hetzner"
Infomaniak models.Provider = "infomaniak"
INWX models.Provider = "inwx"
Ionos models.Provider = "ionos"
Linode models.Provider = "linode"
LuaDNS models.Provider = "luadns"
Namecheap models.Provider = "namecheap"
Expand Down Expand Up @@ -75,6 +76,7 @@ func ProviderChoices() []models.Provider {
Hetzner,
Infomaniak,
INWX,
Ionos,
Linode,
LuaDNS,
Namecheap,
Expand Down
4 changes: 4 additions & 0 deletions internal/provider/headers/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ func SetXAuthUsername(request *http.Request, value string) {
func SetXAuthPassword(request *http.Request, value string) {
request.Header.Set("X-Auth-Password", value)
}

func SetXAPIKey(request *http.Request, value string) {
request.Header.Set("X-API-Key", value)
}
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/qdm12/ddns-updater/internal/provider/providers/hetzner"
"github.com/qdm12/ddns-updater/internal/provider/providers/infomaniak"
"github.com/qdm12/ddns-updater/internal/provider/providers/inwx"
"github.com/qdm12/ddns-updater/internal/provider/providers/ionos"
"github.com/qdm12/ddns-updater/internal/provider/providers/linode"
"github.com/qdm12/ddns-updater/internal/provider/providers/luadns"
"github.com/qdm12/ddns-updater/internal/provider/providers/namecheap"
Expand Down Expand Up @@ -122,6 +123,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
return infomaniak.New(data, domain, host, ipVersion)
case constants.INWX:
return inwx.New(data, domain, host, ipVersion)
case constants.Ionos:
return ionos.New(data, domain, host, ipVersion)
case constants.Linode:
return linode.New(data, domain, host, ipVersion)
case constants.LuaDNS:
Expand Down
33 changes: 33 additions & 0 deletions internal/provider/providers/ionos/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ionos

import (
"net/http"

"github.com/qdm12/ddns-updater/internal/provider/headers"
)

type apiZone struct {
ID string `json:"id"`
Name string `json:"name"`
}

type apiRecord struct {
ID string `json:"id"`
Name string `json:"name"`
RootName string `json:"rootName"`
Type string `json:"type"`
Content string `json:"content"`
TTL uint32 `json:"ttl"`
Prio uint32 `json:"prio"`
Disabled bool `json:"disabled"`
}

func (p *Provider) setHeaders(request *http.Request) {
headers.SetUserAgent(request)
headers.SetAccept(request, "application/json")
headers.SetXAPIKey(request, p.apiKey)
switch request.Method {
case http.MethodPost, http.MethodPut:
headers.SetContentType(request, "application/json")
}
}
78 changes: 78 additions & 0 deletions internal/provider/providers/ionos/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package ionos

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/netip"
"net/url"

"github.com/qdm12/ddns-updater/internal/provider/constants"
"github.com/qdm12/ddns-updater/internal/provider/errors"
)

func (p *Provider) createRecord(ctx context.Context, client *http.Client,
zoneID string, ip netip.Addr) (err error) {
recordType := constants.A
if ip.Is6() {
recordType = constants.AAAA
}

u := url.URL{
Scheme: "https",
Host: "api.hosting.ionos.com",
Path: "/dns/v1/zones/" + zoneID + "/records",
}

const defaultTTL = 3600
const defaultPrio = 0
recordsList := []apiRecord{
{
Name: p.BuildDomainName(),
Type: recordType,
Content: ip.String(),
TTL: defaultTTL,
Prio: defaultPrio,
Disabled: false,
},
}

buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
err = encoder.Encode(recordsList)
if err != nil {
return fmt.Errorf("encoding request data to JSON: %w", err)
}

request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
if err != nil {
return fmt.Errorf("creating http request: %w", err)
}
p.setHeaders(request)

response, err := client.Do(request)
if err != nil {
return fmt.Errorf("doing http request: %w", err)
}
defer response.Body.Close()

switch response.StatusCode {
case http.StatusCreated:
err = response.Body.Close()
if err != nil {
return fmt.Errorf("closing response body: %w", err)
}
return nil
case http.StatusBadRequest:
return fmt.Errorf("%w: %s", errors.ErrBadRequest,
decodeErrorMessage(response.Body))
case http.StatusUnauthorized:
return fmt.Errorf("%w: %s", errors.ErrAuth,
decodeErrorMessage(response.Body))
default:
return fmt.Errorf("%w: %s: %s", errors.ErrHTTPStatusNotValid,
response.Status, decodeErrorMessage(response.Body))
}
}
116 changes: 116 additions & 0 deletions internal/provider/providers/ionos/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package ionos

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"

"github.com/qdm12/ddns-updater/internal/provider/errors"
)

func (p *Provider) getZones(ctx context.Context, client *http.Client) (
zones []apiZone, err error) {
err = p.get(ctx, client, "/zones", nil, &zones)
return zones, err
}

func (p *Provider) getRecords(ctx context.Context, client *http.Client,
zoneID string, recordType string) (records []apiRecord, err error) {
queryParams := url.Values{
"recordName": []string{p.BuildDomainName()},
"recordType": []string{recordType},
}
var responseData struct {
Records []apiRecord `json:"records"`
}
err = p.get(ctx, client, "/zones/"+zoneID, queryParams, &responseData)
if err != nil {
return nil, fmt.Errorf("for zone id %s and type %s: %w",
zoneID, recordType, err)
}

return responseData.Records, nil
}

func (p *Provider) get(ctx context.Context, client *http.Client,
subPath string, queryParams url.Values, responseJSONData any) (err error) {
u := url.URL{
Scheme: "https",
Host: "api.hosting.ionos.com",
Path: filepath.Join("/dns/v1/", subPath),
RawQuery: queryParams.Encode(),
}

request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return fmt.Errorf("creating http request: %w", err)
}
p.setHeaders(request)

response, err := client.Do(request)
if err != nil {
return fmt.Errorf("doing http request: %w", err)
}
defer response.Body.Close()

switch response.StatusCode {
case http.StatusOK:
case http.StatusUnauthorized:
return fmt.Errorf("%w: %s", errors.ErrAuth,
decodeErrorMessage(response.Body))
default:
return fmt.Errorf("%w: %s: %s", errors.ErrHTTPStatusNotValid,
response.Status, decodeErrorMessage(response.Body))
}

decoder := json.NewDecoder(response.Body)
err = decoder.Decode(&responseJSONData)
if err != nil {
return fmt.Errorf("decoding JSON response: %w", err)
}

err = response.Body.Close()
if err != nil {
return fmt.Errorf("closing response body: %w", err)
}

return nil
}

func decodeErrorMessage(body io.Reader) (message string) {
b, err := io.ReadAll(body)
if err != nil {
return fmt.Sprintf("failed reading response body: %s", err)
}

if len(b) == 0 {
return ""
}

var data []struct {
Code string `json:"code"`
Message string `json:"message"`
}
err = json.Unmarshal(b, &data)
if err != nil {
return "failed decoding the following: " + string(b)
}
if len(data) == 0 {
return "no message found"
}

messages := make([]string, len(data))
for i, object := range data {
if object.Message != "" {
messages[i] = object.Message
continue
}
messages[i] = fmt.Sprintf("code %q", object.Code)
}
return strings.Join(messages, "; ")
}
Loading

0 comments on commit ff5767a

Please sign in to comment.