-
-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
478 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Vultr | ||
|
||
## Configuration | ||
|
||
### Example | ||
|
||
```json | ||
{ | ||
"settings": [ | ||
{ | ||
"provider": "vultr", | ||
"domain": "potato.example.com", | ||
"apikey": "AAAAAAAAAAAAAAA", | ||
"ttl": 300, | ||
"ip_version": "ipv4" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
### Compulsory parameters | ||
|
||
- `"domain"` is the domain to update. It can be a root domain (i.e. `example.com`) or a subdomain (i.e. `potato.example.com`) or a wildcard (i.e. `*.example.com`). In case of a wildcard, it only works if there is no existing wildcard records of any record type. | ||
- `"apikey"` is your API key which can be obtained from [my.vultr.com/settings/](https://my.vultr.com/settings/#settingsapi). | ||
|
||
### Optional parameters | ||
|
||
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`. | ||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating. | ||
- `"ttl"` is the record TTL which defaults to 900 seconds. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package vultr | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/netip" | ||
"net/url" | ||
|
||
"github.com/qdm12/ddns-updater/internal/provider/constants" | ||
"github.com/qdm12/ddns-updater/internal/provider/errors" | ||
"github.com/qdm12/ddns-updater/internal/provider/utils" | ||
) | ||
|
||
// https://www.vultr.com/api/#tag/dns/operation/create-dns-domain-record | ||
func (p *Provider) createRecord(ctx context.Context, client *http.Client, ip netip.Addr) (err error) { | ||
recordType := constants.A | ||
if ip.Is6() { | ||
recordType = constants.AAAA | ||
} | ||
|
||
u := url.URL{ | ||
Scheme: "https", | ||
Host: "api.vultr.com", | ||
Path: fmt.Sprintf("/v2/domains/%s/records", p.domain), | ||
} | ||
|
||
requestData := struct { | ||
Type string `json:"type"` | ||
Data string `json:"data"` | ||
Name string `json:"name"` | ||
TTL uint32 `json:"ttl,omitempty"` | ||
}{ | ||
Type: recordType, | ||
Data: ip.String(), | ||
Name: p.owner, | ||
TTL: p.ttl, | ||
} | ||
|
||
buffer := bytes.NewBuffer(nil) | ||
encoder := json.NewEncoder(buffer) | ||
err = encoder.Encode(requestData) | ||
if err != nil { | ||
return fmt.Errorf("json encoding request data: %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 err | ||
} | ||
|
||
bodyBytes, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
_ = response.Body.Close() | ||
return fmt.Errorf("reading response body: %w", err) | ||
} | ||
|
||
err = response.Body.Close() | ||
if err != nil { | ||
return fmt.Errorf("closing response body: %w", err) | ||
} | ||
|
||
switch response.StatusCode { | ||
case http.StatusCreated: | ||
case http.StatusBadRequest: | ||
return fmt.Errorf("%w: %s", errors.ErrBadRequest, parseJSONErrorOrFullBody(bodyBytes)) | ||
case http.StatusUnauthorized, http.StatusForbidden: | ||
return fmt.Errorf("%w: %s", errors.ErrAuth, parseJSONErrorOrFullBody(bodyBytes)) | ||
case http.StatusNotFound: | ||
return fmt.Errorf("%w: %s", errors.ErrDomainNotFound, parseJSONErrorOrFullBody(bodyBytes)) | ||
default: | ||
return fmt.Errorf("%w: %s: %s", errors.ErrHTTPStatusNotValid, | ||
response.Status, parseJSONErrorOrFullBody(bodyBytes)) | ||
} | ||
|
||
errorMessage := parseJSONError(bodyBytes) | ||
if errorMessage != "" { | ||
return fmt.Errorf("%w: %s", errors.ErrUnsuccessful, errorMessage) | ||
} | ||
return nil | ||
} | ||
|
||
// parseJSONErrorOrFullBody parses the json error from a response body | ||
// and returns it if it is not empty. If the json decoding fails OR | ||
// the error parsed is empty, the entire body is returned on a single line. | ||
func parseJSONErrorOrFullBody(body []byte) (message string) { | ||
var parsedJSON struct { | ||
Error string `json:"error"` | ||
} | ||
err := json.Unmarshal(body, &parsedJSON) | ||
if err != nil || parsedJSON.Error == "" { | ||
return utils.ToSingleLine(string(body)) | ||
} | ||
return parsedJSON.Error | ||
} | ||
|
||
// parseJSONError parses the json error from a response body | ||
// and returns it directly. If the json decoding fails, the | ||
// entire body is returned on a single line. | ||
func parseJSONError(body []byte) (message string) { | ||
var parsedJSON struct { | ||
Error string `json:"error"` | ||
} | ||
err := json.Unmarshal(body, &parsedJSON) | ||
if err != nil { | ||
return utils.ToSingleLine(string(body)) | ||
} | ||
return parsedJSON.Error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package vultr | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/netip" | ||
"net/url" | ||
|
||
"github.com/qdm12/ddns-updater/internal/provider/errors" | ||
) | ||
|
||
// https://www.vultr.com/api/#tag/dns/operation/list-dns-domain-records | ||
func (p *Provider) getRecord(ctx context.Context, client *http.Client, | ||
recordType string) (recordID string, recordIP netip.Addr, err error, | ||
) { | ||
u := url.URL{ | ||
Scheme: "https", | ||
Host: "api.vultr.com", | ||
Path: fmt.Sprintf("/v2/domains/%s/records", p.domain), | ||
} | ||
|
||
// max return of get records is 500 records | ||
values := url.Values{} | ||
values.Set("per_page", "500") | ||
u.RawQuery = values.Encode() | ||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) | ||
if err != nil { | ||
return "", netip.Addr{}, fmt.Errorf("creating http request: %w", err) | ||
} | ||
p.setHeaders(request) | ||
|
||
response, err := client.Do(request) | ||
if err != nil { | ||
return "", netip.Addr{}, err | ||
} | ||
|
||
bodyBytes, err := io.ReadAll(response.Body) | ||
if err != nil { | ||
_ = response.Body.Close() | ||
return "", netip.Addr{}, fmt.Errorf("reading response body: %w", err) | ||
} | ||
|
||
err = response.Body.Close() | ||
if err != nil { | ||
return "", netip.Addr{}, fmt.Errorf("closing response body: %w", err) | ||
} | ||
|
||
// todo: implement pagination | ||
var parsedJSON struct { | ||
Error string `json:"error"` | ||
Records []struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Data string `json:"data"` | ||
} `json:"records"` | ||
Meta struct { | ||
Total uint32 `json:"total"` | ||
Links struct { | ||
Next string `json:"next"` | ||
Previous string `json:"prev"` | ||
} `json:"links"` | ||
} `json:"meta"` | ||
} | ||
err = json.Unmarshal(bodyBytes, &parsedJSON) | ||
switch { | ||
case err != nil: | ||
return "", netip.Addr{}, fmt.Errorf("json decoding response body: %w", err) | ||
case response.StatusCode == http.StatusBadRequest: | ||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrBadRequest, parsedJSON.Error) | ||
case response.StatusCode == http.StatusUnauthorized: | ||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrAuth, parsedJSON.Error) | ||
case response.StatusCode == http.StatusNotFound: | ||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrDomainNotFound, parsedJSON.Error) | ||
case response.StatusCode != http.StatusOK: | ||
return "", netip.Addr{}, fmt.Errorf("%w: %d: %s", | ||
errors.ErrHTTPStatusNotValid, response.StatusCode, parseJSONErrorOrFullBody(bodyBytes)) | ||
case parsedJSON.Error != "": | ||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnsuccessful, parsedJSON.Error) | ||
} | ||
|
||
// Status is OK (200) and error field is empty | ||
|
||
for _, record := range parsedJSON.Records { | ||
if record.Name != p.owner || record.Type != recordType { | ||
continue | ||
} | ||
recordIP, err = netip.ParseAddr(record.Data) | ||
if err != nil { | ||
return "", netip.Addr{}, fmt.Errorf("parsing existing IP: %w", err) | ||
} | ||
return record.ID, recordIP, nil | ||
} | ||
|
||
return "", netip.Addr{}, fmt.Errorf("%w: in %d records", errors.ErrRecordNotFound, len(parsedJSON.Records)) | ||
} |
Oops, something went wrong.