mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-05 08:54:09 -04:00
feat(custom): add custom provider
- Sends HTTP GET request to url given with ip information - Configurable ipv4 and ipv6 query parameter keys - Configurable response success detection with a regex - Treat non status OK 200 responses as failures
This commit is contained in:
@@ -173,6 +173,7 @@ Check the documentation for your DNS provider:
|
||||
|
||||
- [Aliyun](https://github.com/qdm12/ddns-updater/blob/master/docs/aliyun.md)
|
||||
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
|
||||
- [Custom](https://github.com/qdm12/ddns-updater/blob/master/docs/custom.md)
|
||||
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
|
||||
- [deSEC](https://github.com/qdm12/ddns-updater/blob/master/docs/desec.md)
|
||||
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
|
||||
|
||||
40
docs/custom.md
Normal file
40
docs/custom.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Custom provider
|
||||
|
||||
The custom provider allows to configure a URL with a few additional parameters to update your records.
|
||||
|
||||
For now it sends an HTTP GET request to the URL given with some additional parameters.
|
||||
Feel free to open issues to extend its configuration options.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "custom",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"url": "https://example.com/update?domain=example.com&host=@&username=username&client_key=client_key",
|
||||
"ipv4key": "ipv4",
|
||||
"ipv6key": "ipv6",
|
||||
"success_regex": "good",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"` is the domain name to update
|
||||
- `"host"` is the host to update, which can be `"@"` (root), `"*"` or a subdomain
|
||||
- `"url"` is the URL to update your records and should contain all the information EXCEPT the IP address to update
|
||||
- `"ipv4key"` is the URL query parameter name for the IPv4 address, for example `ipv4` will be added to the URL with `&ipv4=1.2.3.4`.
|
||||
- `"ipv6key"` is the URL query parameter name for the IPv6 address, for example `ipv6` will be added to the URL with `&ipv6=::aaff`. Even if you don't use IPv6, this must be set to something.
|
||||
- `"success_regex"` is a regular expression to match the response from the server to determine if the update was successful. You can use [regex101.com](https://regex101.com/) to find the regular expression you want. For example `good` would match any response containing the word "good".
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
@@ -7,6 +7,7 @@ const (
|
||||
Aliyun models.Provider = "aliyun"
|
||||
AllInkl models.Provider = "allinkl"
|
||||
Cloudflare models.Provider = "cloudflare"
|
||||
Custom models.Provider = "custom"
|
||||
Dd24 models.Provider = "dd24"
|
||||
DdnssDe models.Provider = "ddnss"
|
||||
DeSEC models.Provider = "desec"
|
||||
|
||||
@@ -17,6 +17,8 @@ var (
|
||||
ErrHostOnlyAt = errors.New(`host can only be "@"`)
|
||||
ErrHostOnlySubdomain = errors.New("host can only be a subdomain")
|
||||
ErrHostWildcard = errors.New(`host cannot be a "*"`)
|
||||
ErrIPv4KeyNotSet = errors.New("IPv4 key is not set")
|
||||
ErrIPv6KeyNotSet = errors.New("IPv6 key is not set")
|
||||
ErrIPv6NotSupported = errors.New("IPv6 is not supported by this provider")
|
||||
ErrKeyNotSet = errors.New("key is not set")
|
||||
ErrKeyNotValid = errors.New("key is not valid")
|
||||
@@ -24,10 +26,13 @@ var (
|
||||
ErrPasswordNotSet = errors.New("password is not set")
|
||||
ErrPasswordNotValid = errors.New("password is not valid")
|
||||
ErrSecretNotSet = errors.New("secret is not set")
|
||||
ErrSuccessRegexNotSet = errors.New("success regex is not set")
|
||||
ErrTokenNotSet = errors.New("token is not set")
|
||||
ErrTokenNotValid = errors.New("token is not valid")
|
||||
ErrTTLNotSet = errors.New("TTL is not set")
|
||||
ErrTTLTooLow = errors.New("TTL is too low")
|
||||
ErrURLNotHTTPS = errors.New("url is not https")
|
||||
ErrURLNotSet = errors.New("url is not set")
|
||||
ErrUsernameNotSet = errors.New("username is not set")
|
||||
ErrUsernameNotValid = errors.New("username is not valid")
|
||||
ErrUserServiceKeyNotValid = errors.New("user service key is not valid")
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/aliyun"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/allinkl"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/cloudflare"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/custom"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/dd24"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/ddnss"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/desec"
|
||||
@@ -79,6 +80,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
|
||||
return allinkl.New(data, domain, host, ipVersion)
|
||||
case constants.Cloudflare:
|
||||
return cloudflare.New(data, domain, host, ipVersion)
|
||||
case constants.Custom:
|
||||
return custom.New(data, domain, host, ipVersion)
|
||||
case constants.Dd24:
|
||||
return dd24.New(data, domain, host, ipVersion)
|
||||
case constants.DdnssDe:
|
||||
|
||||
156
internal/provider/providers/custom/provider.go
Normal file
156
internal/provider/providers/custom/provider.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package custom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
url *url.URL
|
||||
ipv4Key string
|
||||
ipv6Key string
|
||||
successRegex regexp.Regexp
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
ipVersion ipversion.IPVersion) (p *Provider, err error) {
|
||||
extraSettings := struct {
|
||||
URL string `json:"url"`
|
||||
IPv4Key string `json:"ipv4key"`
|
||||
IPv6Key string `json:"ipv6key"`
|
||||
SuccessRegex regexp.Regexp `json:"success_regex"`
|
||||
}{}
|
||||
err = json.Unmarshal(data, &extraSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("JSON decoding provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(extraSettings.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing URL: %w", err)
|
||||
}
|
||||
|
||||
p = &Provider{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
url: parsedURL,
|
||||
ipv4Key: extraSettings.IPv4Key,
|
||||
ipv6Key: extraSettings.IPv6Key,
|
||||
successRegex: extraSettings.SuccessRegex,
|
||||
}
|
||||
err = p.isValid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Provider) isValid() error {
|
||||
switch {
|
||||
case p.url.String() == "":
|
||||
return fmt.Errorf("%w", errors.ErrURLNotSet)
|
||||
case p.url.Scheme != "https":
|
||||
return fmt.Errorf("%w: %s", errors.ErrURLNotHTTPS, p.url.Scheme)
|
||||
case p.ipv4Key == "":
|
||||
return fmt.Errorf("%w", errors.ErrIPv4KeyNotSet)
|
||||
case p.ipv6Key == "":
|
||||
return fmt.Errorf("%w", errors.ErrIPv6KeyNotSet)
|
||||
case p.successRegex.String() == "":
|
||||
return fmt.Errorf("%w", errors.ErrSuccessRegexNotSet)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return utils.ToString(p.domain, p.host, constants.Custom, p.ipVersion)
|
||||
}
|
||||
|
||||
func (p *Provider) Domain() string {
|
||||
return p.domain
|
||||
}
|
||||
|
||||
func (p *Provider) Host() string {
|
||||
return p.host
|
||||
}
|
||||
|
||||
func (p *Provider) IPVersion() ipversion.IPVersion {
|
||||
return p.ipVersion
|
||||
}
|
||||
|
||||
func (p *Provider) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Provider) BuildDomainName() string {
|
||||
return utils.BuildDomainName(p.host, p.domain)
|
||||
}
|
||||
|
||||
func (p *Provider) HTML() models.HTMLRow {
|
||||
updateHostname := p.url.Hostname()
|
||||
return models.HTMLRow{
|
||||
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
|
||||
Host: p.Host(),
|
||||
Provider: fmt.Sprintf("<a href=\"https://%s/\">%s: %s</a>",
|
||||
updateHostname, constants.Custom, updateHostname),
|
||||
IPVersion: p.ipVersion.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
values := url.Values{}
|
||||
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
|
||||
ipKey := p.ipv4Key
|
||||
if ip.Is6() {
|
||||
ipKey = p.ipv6Key
|
||||
}
|
||||
values.Set(ipKey, ip.String())
|
||||
p.url.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, p.url.String(), nil)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
headers.SetUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
|
||||
}
|
||||
|
||||
if p.successRegex.MatchString(s) {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse,
|
||||
utils.ToSingleLine(s))
|
||||
}
|
||||
Reference in New Issue
Block a user