mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-07-28 16:42:37 -04:00
feat(providers): namesilo.com support (#866)
- Credits to @Zeustopher for writing most of the readme
This commit is contained in:
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -32,6 +32,9 @@
|
||||
},
|
||||
{
|
||||
"pattern": "^https://my.vultr.com/settings/#settingsapi$"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://www.namesilo.com"
|
||||
}
|
||||
],
|
||||
"timeout": "20s",
|
||||
|
||||
@@ -81,6 +81,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog
|
||||
- Myaddr
|
||||
- Name.com
|
||||
- Namecheap
|
||||
- NameSilo
|
||||
- Netcup
|
||||
- NoIP
|
||||
- Now-DNS
|
||||
@@ -247,6 +248,7 @@ Check the documentation for your DNS provider:
|
||||
- [Myaddr](docs/myaddr.md)
|
||||
- [Name.com](docs/name.com.md)
|
||||
- [Namecheap](docs/namecheap.md)
|
||||
- [NameSilo](docs/namesilo.md)
|
||||
- [Netcup](docs/netcup.md)
|
||||
- [NoIP](docs/noip.md)
|
||||
- [Now-DNS](docs/nowdns.md)
|
||||
|
||||
51
docs/namesilo.md
Normal file
51
docs/namesilo.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# NameSilo
|
||||
|
||||
[](https://www.namesilo.com)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namesilo",
|
||||
"domain": "sub.example.com",
|
||||
"key": "71dZaE8c2Aa926Ca2E8c1",
|
||||
"ttl": 7207,
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"` is the domain to update. It can be `example.com` (root domain), `sub.example.com` (subdomain of `example.com`) or `*.example.com` for the wildcard.
|
||||
- `"key"` is the NameSilo API Key obtained using the domain setup instructions below. For example: `71dZaE8c2Aa926Ca2E8c1`.
|
||||
|
||||
### 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's Time to Live (TTL), which defaults to `7207` seconds. It must be numeric, less than `2592001`, and greater than or equal to `3600`. TTL values of `3603` or `7207` may be subject to NameSilo's [Automatic TTL Adjustments](https://www.namesilo.com/support/v2/articles/domain-manager/dns-manager#auto_ttl).
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Login to the [Namesilo API Manager](https://www.namesilo.com/account/api-manager) with your account credentials.
|
||||
1. Generate an API key. The generated API key will look similar to `71dZaE8c2Aa926Ca2E8c1`.
|
||||
- (do _not_ check the "Generate key for read-only access" box)
|
||||
|
||||
[](https://www.namesilo.com/account/api-manager)
|
||||
[](https://www.namesilo.com/account/api-manager)
|
||||
|
||||
## Testing
|
||||
|
||||
1. Go to the [NameSilo Domain Manager](https://www.namesilo.com/account_domains.php).
|
||||
1. Choose "Manage DNS for this domain" (the globe icon) for the domain you wish to test.
|
||||
[](https://www.namesilo.com/account_domains.php)
|
||||
|
||||
1. Change the IP address of the host to `127.0.0.1`.
|
||||
1. Run the ddns-updater.
|
||||
1. Refresh the Namesilo webpage to check the update occurred.
|
||||
@@ -40,6 +40,7 @@ const (
|
||||
Myaddr models.Provider = "myaddr"
|
||||
Namecheap models.Provider = "namecheap"
|
||||
NameCom models.Provider = "name.com"
|
||||
NameSilo models.Provider = "namesilo"
|
||||
Netcup models.Provider = "netcup"
|
||||
Njalla models.Provider = "njalla"
|
||||
NoIP models.Provider = "noip"
|
||||
@@ -94,6 +95,7 @@ func ProviderChoices() []models.Provider {
|
||||
Myaddr,
|
||||
Namecheap,
|
||||
NameCom,
|
||||
NameSilo,
|
||||
Njalla,
|
||||
NoIP,
|
||||
NowDNS,
|
||||
|
||||
@@ -30,6 +30,7 @@ var (
|
||||
ErrTokenNotValid = errors.New("token is not valid")
|
||||
ErrTTLNotSet = errors.New("TTL is not set")
|
||||
ErrTTLTooLow = errors.New("TTL is too low")
|
||||
ErrTTLTooHigh = errors.New("TTL is too high")
|
||||
ErrURLNotHTTPS = errors.New("url is not https")
|
||||
ErrURLNotSet = errors.New("url is not set")
|
||||
ErrUsernameNotSet = errors.New("username is not set")
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/myaddr"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/namecheap"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/namecom"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/namesilo"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/netcup"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/njalla"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/noip"
|
||||
@@ -155,6 +156,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
|
||||
return namecheap.New(data, domain, owner)
|
||||
case constants.NameCom:
|
||||
return namecom.New(data, domain, owner, ipVersion, ipv6Suffix)
|
||||
case constants.NameSilo:
|
||||
return namesilo.New(data, domain, owner, ipVersion, ipv6Suffix)
|
||||
case constants.Netcup:
|
||||
return netcup.New(data, domain, owner, ipVersion, ipv6Suffix)
|
||||
case constants.Njalla:
|
||||
|
||||
318
internal/provider/providers/namesilo/provider.go
Normal file
318
internal/provider/providers/namesilo/provider.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package namesilo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"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
|
||||
owner string
|
||||
ipVersion ipversion.IPVersion
|
||||
ipv6Suffix netip.Prefix
|
||||
key string
|
||||
ttl *uint32
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Reply struct {
|
||||
Code json.Number `json:"code"`
|
||||
Detail string `json:"detail"`
|
||||
Records []struct {
|
||||
ID string `json:"record_id"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Value string `json:"value"`
|
||||
} `json:"resource_record,omitempty"` // Field only available during list record
|
||||
} `json:"reply"`
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, owner string,
|
||||
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
|
||||
provider *Provider, err error,
|
||||
) {
|
||||
var providerSpecificSettings struct {
|
||||
Key string `json:"key"`
|
||||
TTL *uint32 `json:"ttl,omitempty"`
|
||||
}
|
||||
err = json.Unmarshal(data, &providerSpecificSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
err = validateSettings(domain, providerSpecificSettings.Key, providerSpecificSettings.TTL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
domain: domain,
|
||||
owner: owner,
|
||||
ipVersion: ipVersion,
|
||||
ipv6Suffix: ipv6Suffix,
|
||||
key: providerSpecificSettings.Key,
|
||||
ttl: providerSpecificSettings.TTL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateSettings(domain, key string, ttl *uint32) (err error) {
|
||||
err = utils.CheckDomain(domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
|
||||
}
|
||||
|
||||
const (
|
||||
minTTL = uint32(3600)
|
||||
maxTTL = uint32(2592001)
|
||||
)
|
||||
switch {
|
||||
case key == "":
|
||||
return fmt.Errorf("%w", errors.ErrAPIKeyNotSet)
|
||||
case ttl != nil && *ttl < minTTL:
|
||||
return fmt.Errorf("%w: %d must be at least %d", errors.ErrTTLTooLow, *ttl, minTTL)
|
||||
case ttl != nil && *ttl > maxTTL:
|
||||
return fmt.Errorf("%w: %d must be at most %d", errors.ErrTTLTooHigh, *ttl, maxTTL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return utils.ToString(p.domain, p.owner, constants.NameSilo, p.ipVersion)
|
||||
}
|
||||
|
||||
func (p *Provider) Domain() string {
|
||||
return p.domain
|
||||
}
|
||||
|
||||
func (p *Provider) Owner() string {
|
||||
return p.owner
|
||||
}
|
||||
|
||||
func (p *Provider) IPVersion() ipversion.IPVersion {
|
||||
return p.ipVersion
|
||||
}
|
||||
|
||||
func (p *Provider) IPv6Suffix() netip.Prefix {
|
||||
return p.ipv6Suffix
|
||||
}
|
||||
|
||||
func (p *Provider) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Provider) BuildDomainName() string {
|
||||
return utils.BuildDomainName(p.owner, p.domain)
|
||||
}
|
||||
|
||||
func (p *Provider) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
|
||||
Owner: p.Owner(),
|
||||
Provider: "<a href=\"https://www.namesilo.com/\">NameSilo</a>",
|
||||
IPVersion: p.ipVersion.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Update does the following:
|
||||
// 1. if there's no record, create it.
|
||||
// 2. if it exists and ip is different, update it.
|
||||
// 3. if it exists and ip is the same, do nothing.
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, newIP netip.Addr) (netip.Addr, error) {
|
||||
recordType := constants.A
|
||||
if newIP.Is6() {
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
|
||||
recordID, currentIP, err := p.getRecord(ctx, client, recordType)
|
||||
|
||||
if stderrors.Is(err, errors.ErrRecordNotFound) {
|
||||
if err := p.createRecord(ctx, client, recordType, newIP); err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating record: %w", err)
|
||||
}
|
||||
return newIP, nil
|
||||
} else if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("retrieving records: %w", err)
|
||||
}
|
||||
|
||||
if currentIP != newIP {
|
||||
if err := p.updateRecord(ctx, client, recordID, newIP); err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("updating record: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return newIP, nil
|
||||
}
|
||||
|
||||
// https://www.namesilo.com/api-reference#dns/dns-list-records
|
||||
func (p *Provider) getRecord(ctx context.Context, client *http.Client, recordType string) (
|
||||
id *string, ip netip.Addr, err error,
|
||||
) {
|
||||
queryParams := url.Values{}
|
||||
url := p.createRequestURL("/api/dnsListRecords", queryParams)
|
||||
|
||||
response, err := p.sendAPIRequest(ctx, client, url)
|
||||
if err != nil {
|
||||
return nil, netip.Addr{}, err
|
||||
}
|
||||
|
||||
// find matching record
|
||||
host := utils.BuildURLQueryHostname(p.owner, p.domain)
|
||||
for _, record := range response.Reply.Records {
|
||||
if record.Host != host || record.Type != recordType {
|
||||
continue
|
||||
}
|
||||
ip, err = netip.ParseAddr(record.Value)
|
||||
if err != nil {
|
||||
return nil, netip.Addr{}, fmt.Errorf("parsing existing IP: %w", err)
|
||||
}
|
||||
return &record.ID, ip, nil
|
||||
}
|
||||
|
||||
return nil, netip.Addr{}, fmt.Errorf("%w", errors.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
// https://www.namesilo.com/api-reference#dns/dns-add-record
|
||||
func (p *Provider) createRecord(
|
||||
ctx context.Context,
|
||||
client *http.Client,
|
||||
recordType string,
|
||||
ip netip.Addr,
|
||||
) error {
|
||||
const path = "/api/dnsAddRecord"
|
||||
queryParams := p.buildRecordParams(ip)
|
||||
queryParams.Set("rrtype", recordType)
|
||||
|
||||
url := p.createRequestURL(path, queryParams)
|
||||
|
||||
_, err := p.sendAPIRequest(ctx, client, url)
|
||||
return err
|
||||
}
|
||||
|
||||
// https://www.namesilo.com/api-reference#dns/dns-update-record
|
||||
func (p *Provider) updateRecord(
|
||||
ctx context.Context,
|
||||
client *http.Client,
|
||||
recordID *string,
|
||||
ip netip.Addr,
|
||||
) error {
|
||||
const path = "/api/dnsUpdateRecord"
|
||||
queryParams := p.buildRecordParams(ip)
|
||||
queryParams.Set("rrid", *recordID)
|
||||
|
||||
url := p.createRequestURL(path, queryParams)
|
||||
|
||||
_, err := p.sendAPIRequest(ctx, client, url)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and populate common query params for requests that modify a single record (ie. add or update).
|
||||
func (p *Provider) buildRecordParams(ip netip.Addr) url.Values {
|
||||
name := p.owner
|
||||
if name == "@" {
|
||||
name = ""
|
||||
}
|
||||
|
||||
queryParams := url.Values{
|
||||
"rrhost": {name},
|
||||
"rrvalue": {ip.String()},
|
||||
}
|
||||
if p.ttl != nil {
|
||||
queryParams.Set("rrttl", strconv.FormatUint(uint64(*p.ttl), 10))
|
||||
}
|
||||
|
||||
return queryParams
|
||||
}
|
||||
|
||||
func (p *Provider) createRequestURL(path string, queryParams url.Values) string {
|
||||
baseURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.namesilo.com",
|
||||
Path: path,
|
||||
}
|
||||
queryParams.Set("version", "1")
|
||||
queryParams.Set("type", "json")
|
||||
queryParams.Set("key", p.key)
|
||||
queryParams.Set("domain", p.domain)
|
||||
baseURL.RawQuery = queryParams.Encode()
|
||||
return baseURL.String()
|
||||
}
|
||||
|
||||
func (p *Provider) sendAPIRequest(ctx context.Context, client *http.Client, url string) (*apiResponse, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
headers.SetUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(string(data)))
|
||||
}
|
||||
|
||||
var parsedResponse apiResponse
|
||||
err = json.Unmarshal(data, &parsedResponse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json decoding response body: %w", err)
|
||||
}
|
||||
|
||||
err = p.validateResponseCode(parsedResponse.Reply.Code, parsedResponse.Reply.Detail)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating reply code: %w", err)
|
||||
}
|
||||
return &parsedResponse, nil
|
||||
}
|
||||
|
||||
// https://www.namesilo.com/api-reference (Response Codes)
|
||||
func (p *Provider) validateResponseCode(code json.Number, detail string) error {
|
||||
// The API inconsistently swaps between number and string typing for the code field,
|
||||
// but the value should always be an integer.
|
||||
parsedCode, err := code.Int64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing response code: %w", err)
|
||||
}
|
||||
|
||||
codeToError := map[int64]error{
|
||||
300: nil, // Successful API operation
|
||||
110: errors.ErrKeyNotValid, // Invalid API key
|
||||
112: errors.ErrFeatureUnavailable, // API not available to Sub-Accounts
|
||||
113: errors.ErrBannedAbuse, // API account cannot be accessed from your IP
|
||||
200: errors.ErrDomainDisabled, // Domain is not active, or does not belong to this user
|
||||
201: errors.ErrDNSServerSide, // Internal system error
|
||||
210: errors.ErrUnsuccessful, // General error (details in response)
|
||||
280: errors.ErrBadRequest, // DNS modification error
|
||||
}
|
||||
|
||||
if err, exists := codeToError[parsedCode]; exists {
|
||||
if err == nil {
|
||||
return nil // Successful operation, no error to return
|
||||
}
|
||||
return fmt.Errorf("%w: %d: %s", err, parsedCode, detail)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %d: %s", errors.ErrUnknownResponse, parsedCode, detail)
|
||||
}
|
||||
BIN
readme/namesilo.jpg
Normal file
BIN
readme/namesilo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
readme/namesilo1.jpg
Normal file
BIN
readme/namesilo1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
readme/namesilo2.jpg
Normal file
BIN
readme/namesilo2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
readme/namesilo3.jpg
Normal file
BIN
readme/namesilo3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Reference in New Issue
Block a user