mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-10-21 11:16:39 +08:00
NEW PROVIDER: CentralNic Reseller (CNR) - formerly RRPProxy (#3203)
This commit is contained in:
parent
be48b6e72f
commit
b2ee265578
15 changed files with 778 additions and 1 deletions
7
.github/workflows/pr_test.yml
vendored
7
.github/workflows/pr_test.yml
vendored
|
@ -88,7 +88,7 @@ jobs:
|
||||||
Write-Host "Integration test providers: $Providers"
|
Write-Host "Integration test providers: $Providers"
|
||||||
echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
|
echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT
|
||||||
env:
|
env:
|
||||||
PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','HUAWEICLOUD','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']"
|
PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','HUAWEICLOUD','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']"
|
||||||
ENV_CONTEXT: ${{ toJson(env) }}
|
ENV_CONTEXT: ${{ toJson(env) }}
|
||||||
VARS_CONTEXT: ${{ toJson(vars) }}
|
VARS_CONTEXT: ${{ toJson(vars) }}
|
||||||
SECRETS_CONTEXT: ${{ toJson(secrets) }}
|
SECRETS_CONTEXT: ${{ toJson(secrets) }}
|
||||||
|
@ -111,6 +111,7 @@ jobs:
|
||||||
BUNNY_DNS_DOMAIN: ${{ vars.BUNNY_DNS_DOMAIN }}
|
BUNNY_DNS_DOMAIN: ${{ vars.BUNNY_DNS_DOMAIN }}
|
||||||
CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
|
CLOUDFLAREAPI_DOMAIN: ${{ vars.CLOUDFLAREAPI_DOMAIN }}
|
||||||
CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
|
CLOUDNS_DOMAIN: ${{ vars.CLOUDNS_DOMAIN }}
|
||||||
|
CNR_DOMAIN: ${{ vars.CNR_DOMAIN }}
|
||||||
CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
|
CSCGLOBAL_DOMAIN: ${{ vars.CSCGLOBAL_DOMAIN }}
|
||||||
DIGITALOCEAN_DOMAIN: ${{ vars.DIGITALOCEAN_DOMAIN }}
|
DIGITALOCEAN_DOMAIN: ${{ vars.DIGITALOCEAN_DOMAIN }}
|
||||||
GANDI_V5_DOMAIN: ${{ vars.GANDI_V5_DOMAIN }}
|
GANDI_V5_DOMAIN: ${{ vars.GANDI_V5_DOMAIN }}
|
||||||
|
@ -146,6 +147,10 @@ jobs:
|
||||||
CSCGLOBAL_APIKEY: ${{ secrets.CSCGLOBAL_APIKEY }}
|
CSCGLOBAL_APIKEY: ${{ secrets.CSCGLOBAL_APIKEY }}
|
||||||
CSCGLOBAL_USERTOKEN: ${{ secrets.CSCGLOBAL_USERTOKEN }}
|
CSCGLOBAL_USERTOKEN: ${{ secrets.CSCGLOBAL_USERTOKEN }}
|
||||||
#
|
#
|
||||||
|
CNR_UID: ${{ secrets.CNR_UID }}
|
||||||
|
CNR_PW: ${{ secrets.CNR_PW }}
|
||||||
|
CNR_ENTITY: ${{ secrets.CNR_ENTITY }}
|
||||||
|
#
|
||||||
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }}
|
||||||
#
|
#
|
||||||
GANDI_V5_APIKEY: ${{ secrets.GANDI_V5_APIKEY }}
|
GANDI_V5_APIKEY: ${{ secrets.GANDI_V5_APIKEY }}
|
||||||
|
|
1
OWNERS
1
OWNERS
|
@ -7,6 +7,7 @@ providers/bind @tlimoncelli
|
||||||
providers/bunnydns @ppmathis
|
providers/bunnydns @ppmathis
|
||||||
providers/cloudflare @tresni
|
providers/cloudflare @tresni
|
||||||
providers/cloudns @pragmaton
|
providers/cloudns @pragmaton
|
||||||
|
providers/cnr @KaiSchwarz-cnic
|
||||||
providers/cscglobal @mikenz
|
providers/cscglobal @mikenz
|
||||||
providers/desec @D3luxee
|
providers/desec @D3luxee
|
||||||
providers/digitalocean @Deraen
|
providers/digitalocean @Deraen
|
||||||
|
|
|
@ -25,6 +25,7 @@ Currently supported DNS providers:
|
||||||
- Bunny DNS
|
- Bunny DNS
|
||||||
- Cloudflare
|
- Cloudflare
|
||||||
- ClouDNS
|
- ClouDNS
|
||||||
|
- CentralNic Reseller (CNR) - formerly RRPProxy
|
||||||
- deSEC
|
- deSEC
|
||||||
- DigitalOcean
|
- DigitalOcean
|
||||||
- DNS Made Easy
|
- DNS Made Easy
|
||||||
|
@ -66,6 +67,7 @@ Currently supported Domain Registrars:
|
||||||
|
|
||||||
- AWS Route 53
|
- AWS Route 53
|
||||||
- CSC Global
|
- CSC Global
|
||||||
|
- CentralNic Reseller (formerly RRPProxy)
|
||||||
- DNSOVERHTTPS
|
- DNSOVERHTTPS
|
||||||
- Dynadot
|
- Dynadot
|
||||||
- easyname
|
- easyname
|
||||||
|
|
112
documentation/provider/cnr.md
Normal file
112
documentation/provider/cnr.md
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
CentralNic Reseller (CNR), formerly known as RRPProxy, is a prominent provider of domain registration and DNS solutions. Trusted by individuals, service providers, and registrars around the world, CNR is recognized for its cutting-edge technology, exceptional performance, and reliable uptime.
|
||||||
|
|
||||||
|
Our advanced DNS expertise is integral to our offering. With CentralNic Reseller, you benefit from a leading DNS platform that features robust DNS automation, DNSSEC for enhanced security, and PremiumDNS via our Anycast Network. Additionally, our platform supports a comprehensive set of features, as detailed by DNSControl.
|
||||||
|
|
||||||
|
This is based on API documents found at [https://kb.centralnicreseller.com/api/api-commands/api-command-reference#cat-dynamicdns](https://kb.centralnicreseller.com/api/api-commands/api-command-reference#cat-dynamicdns)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To use this provider, add an entry to `creds.json` with `TYPE` set to `CNR`
|
||||||
|
along with your CentralNic Reseller login data.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
{% code title="creds.json" %}
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"CNR": {
|
||||||
|
"TYPE": "CNR",
|
||||||
|
"apilogin": "your-cnr-account-id",
|
||||||
|
"apipassword": "your-cnr-account-password",
|
||||||
|
"apientity": "LIVE", // for the LIVE system; use "OTE" for the OT&E system
|
||||||
|
"debugmode": "0", // set it to "1" to get debug output of the communication with our Backend System API
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
{% endcode %}
|
||||||
|
|
||||||
|
Here a working example for our OT&E System:
|
||||||
|
|
||||||
|
{% code title="creds.json" %}
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"CNR": {
|
||||||
|
"TYPE": "CNR",
|
||||||
|
"apilogin": "YourUserName",
|
||||||
|
"apipassword": "YourPassword",
|
||||||
|
"apientity": "OTE",
|
||||||
|
"debugmode": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
{% endcode %}
|
||||||
|
|
||||||
|
{% hint style="info" %}
|
||||||
|
**NOTE**: The above credentials are known to the public.
|
||||||
|
{% endhint %}
|
||||||
|
|
||||||
|
With the above CentralNic Reseller entry in `creds.json`, you can run the
|
||||||
|
integration tests as follows:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dnscontrol get-zones --format=nameonly cnr CNR all
|
||||||
|
```
|
||||||
|
```shell
|
||||||
|
# Review the output. Pick one domain and set CNR_DOMAIN.
|
||||||
|
export CNR_DOMAIN=yodream.com # Pick a domain name.
|
||||||
|
export CNR_ENTITY=OTE
|
||||||
|
export CNR_UID=test.user
|
||||||
|
export CNR_PW=test.passw0rd
|
||||||
|
cd integrationTest # NOTE: Not needed if already in that subdirectory
|
||||||
|
go test -v -verbose -provider CNR
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Here's an example DNS Configuration `dnsconfig.js` using our provider module.
|
||||||
|
Even though it shows how you use us as Domain Registrar AND DNS Provider, we don't force you to do that.
|
||||||
|
You are free to decide if you want to use both of our provider technology or just one of them.
|
||||||
|
|
||||||
|
{% code title="dnsconfig.js" %}
|
||||||
|
```javascript
|
||||||
|
var REG_CNR = NewRegistrar("CNR");
|
||||||
|
var DSP_CNR = NewDnsProvider("CNR");
|
||||||
|
|
||||||
|
// Set Default TTL for all RR to reflect our Backend API Default
|
||||||
|
// If you use additional DNS Providers, configure a default TTL
|
||||||
|
// per domain using the domain modifier DefaultTTL instead.
|
||||||
|
// also check this issue for [NAMESERVER TTL](https://github.com/StackExchange/dnscontrol/issues/176).
|
||||||
|
DEFAULTS(
|
||||||
|
{"ns_ttl":"3600"},
|
||||||
|
DefaultTTL(3600)
|
||||||
|
);
|
||||||
|
|
||||||
|
D("example.com", REG_CNR, DnsProvider(DSP_CNR),
|
||||||
|
NAMESERVER("ns1.rrpproxy.net"),
|
||||||
|
NAMESERVER("ns2.rrpproxy.net"),
|
||||||
|
NAMESERVER("ns3.rrpproxy.net"),
|
||||||
|
NAMESERVER("ns4.rrpproxy.net"),
|
||||||
|
A("elk1", "10.190.234.178"),
|
||||||
|
A("test", "56.123.54.12"),
|
||||||
|
END);
|
||||||
|
```
|
||||||
|
{% endcode %}
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
This provider does not recognize any special metadata fields unique to CentralNic Reseller (CNR).
|
||||||
|
|
||||||
|
## get-zones
|
||||||
|
|
||||||
|
`dnscontrol get-zones` is implemented for this provider. The list
|
||||||
|
includes both basic and premier zones.
|
||||||
|
|
||||||
|
## New domains
|
||||||
|
|
||||||
|
If a dnszone does not exist in your CNR account, DNSControl will *not* automatically add it with the `dnscontrol push` or `dnscontrol preview` command. You'll need to do that via the control panel manually or using the command `dnscontrol create-domains`.
|
||||||
|
This is because it could lead to unwanted costs on customer-side that we want to avoid.
|
||||||
|
|
||||||
|
## Debug Mode
|
||||||
|
|
||||||
|
As shown in the configuration examples above, this can be activated on demand and it can be used to check the API commands send to our system.
|
||||||
|
In general this is thought for our purpose to have an easy way to dive into issues. But if you're interested what's going on, feel free to activate it.
|
|
@ -23,6 +23,7 @@ If a feature is definitively not supported for whatever reason, we would also li
|
||||||
| [`BUNNY_DNS`](provider/bunny_dns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❔ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❔ | ❌ | ❌ | ❌ | ❔ | ❔ | ❌ | ✅ | ✅ |
|
| [`BUNNY_DNS`](provider/bunny_dns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❔ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❔ | ❌ | ❌ | ❌ | ❔ | ❔ | ❌ | ✅ | ✅ |
|
||||||
| [`CLOUDFLAREAPI`](provider/cloudflareapi.md) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ✅ |
|
| [`CLOUDFLAREAPI`](provider/cloudflareapi.md) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | ✅ |
|
||||||
| [`CLOUDNS`](provider/cloudns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ❔ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ✅ | ❔ | ❔ | ✅ | ✅ |
|
| [`CLOUDNS`](provider/cloudns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ❔ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ✅ | ❔ | ❔ | ✅ | ✅ |
|
||||||
|
| [`CNR`](provider/cnr.md) | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ |
|
||||||
| [`CSCGLOBAL`](provider/cscglobal.md) | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ |
|
| [`CSCGLOBAL`](provider/cscglobal.md) | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ |
|
||||||
| [`DESEC`](provider/desec.md) | ❌ | ✅ | ❌ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ✅ | ❔ | ❔ | ✅ | ❔ | ✅ | ✅ |
|
| [`DESEC`](provider/desec.md) | ❌ | ✅ | ❌ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ✅ | ❔ | ❔ | ✅ | ❔ | ✅ | ✅ |
|
||||||
| [`DIGITALOCEAN`](provider/digitalocean.md) | ❌ | ✅ | ❌ | ✅ | ❔ | ✅ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ |
|
| [`DIGITALOCEAN`](provider/digitalocean.md) | ❌ | ✅ | ❌ | ✅ | ❔ | ✅ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ |
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -59,6 +59,7 @@ require (
|
||||||
require (
|
require (
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
|
||||||
github.com/G-Core/gcore-dns-sdk-go v0.2.9
|
github.com/G-Core/gcore-dns-sdk-go v0.2.9
|
||||||
|
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.0
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/fbiville/markdown-table-formatter v0.3.0
|
github.com/fbiville/markdown-table-formatter v0.3.0
|
||||||
github.com/go-acme/lego/v4 v4.20.2
|
github.com/go-acme/lego/v4 v4.20.2
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -91,6 +91,8 @@ github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7 h1:Jk7u
|
||||||
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7/go.mod h1:FnQtD0+Q/1NZxi0eEWN+3ZRyMsE9vzSB3YjyunkbKD0=
|
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 v4.0.7/go.mod h1:FnQtD0+Q/1NZxi0eEWN+3ZRyMsE9vzSB3YjyunkbKD0=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.0 h1:45FDlPw2mCKrP3C3i0mACQpnG14k3z6ZhDX853idMHw=
|
||||||
|
github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.0/go.mod h1:gDHPM5Nia+C/Q4Uw5rn9i+OIP3S06WUe7RdCpNP2C+E=
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
|
|
@ -72,6 +72,14 @@
|
||||||
"notification_emails": "$CSCGLOBAL_NOTIFICATION",
|
"notification_emails": "$CSCGLOBAL_NOTIFICATION",
|
||||||
"user-token": "$CSCGLOBAL_USERTOKEN"
|
"user-token": "$CSCGLOBAL_USERTOKEN"
|
||||||
},
|
},
|
||||||
|
"CNR": {
|
||||||
|
"TYPE": "CNR",
|
||||||
|
"apientity": "$CNR_ENTITY",
|
||||||
|
"apilogin": "$CNR_UID",
|
||||||
|
"apipassword": "$CNR_PW",
|
||||||
|
"debugmode": "$CNR_DEBUGMODE",
|
||||||
|
"domain": "$CNR_DOMAIN"
|
||||||
|
},
|
||||||
"DESEC": {
|
"DESEC": {
|
||||||
"TYPE": "DESEC",
|
"TYPE": "DESEC",
|
||||||
"auth-token": "$DESEC_TOKEN",
|
"auth-token": "$DESEC_TOKEN",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/azureprivatedns"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/azureprivatedns"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/bind"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/bind"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/bunnydns"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/bunnydns"
|
||||||
|
_ "github.com/StackExchange/dnscontrol/v4/providers/cnr"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudflare"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudns"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/cloudns"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/cscglobal"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/cscglobal"
|
||||||
|
|
21
providers/cnr/auditrecords.go
Normal file
21
providers/cnr/auditrecords.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package cnr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditRecords returns a list of errors corresponding to the records
|
||||||
|
// that aren't supported by this provider. If all records are
|
||||||
|
// supported, an empty list is returned.
|
||||||
|
func AuditRecords(records []*models.RecordConfig) []error {
|
||||||
|
a := rejectif.Auditor{}
|
||||||
|
|
||||||
|
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-10-01
|
||||||
|
|
||||||
|
a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-11-30
|
||||||
|
|
||||||
|
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28
|
||||||
|
|
||||||
|
return a.Audit(records)
|
||||||
|
}
|
83
providers/cnr/cnrProvider.go
Normal file
83
providers/cnr/cnrProvider.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Package CNR implements a registrar that uses the CNR api to set name servers. It will self register it's providers when imported.
|
||||||
|
package cnr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||||
|
cnrcl "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/apiclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GoReleaser: version
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CNRClient describes a connection to the CNR API.
|
||||||
|
type CNRClient struct {
|
||||||
|
conf map[string]string
|
||||||
|
APILogin string
|
||||||
|
APIPassword string
|
||||||
|
APIEntity string
|
||||||
|
client *cnrcl.APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
var features = providers.DocumentationNotes{
|
||||||
|
// The default for unlisted capabilities is 'Cannot'.
|
||||||
|
// See providers/capabilities.go for the entire list of capabilities.
|
||||||
|
providers.CanGetZones: providers.Can(),
|
||||||
|
providers.CanConcur: providers.Can(),
|
||||||
|
providers.CanUseAlias: providers.Cannot("Not supported. You may use CNAME records instead. An Alternative solution is planned."),
|
||||||
|
providers.CanUseCAA: providers.Can(),
|
||||||
|
providers.CanUseLOC: providers.Unimplemented(),
|
||||||
|
providers.CanUsePTR: providers.Can(),
|
||||||
|
providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"),
|
||||||
|
providers.CanUseTLSA: providers.Can(),
|
||||||
|
providers.DocCreateDomains: providers.Can(),
|
||||||
|
providers.DocDualHost: providers.Can(),
|
||||||
|
providers.DocOfficiallySupported: providers.Cannot("Actively maintained provider module."),
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProvider(conf map[string]string) (*CNRClient, error) {
|
||||||
|
api := &CNRClient{
|
||||||
|
conf: conf,
|
||||||
|
client: cnrcl.NewAPIClient(),
|
||||||
|
}
|
||||||
|
api.client.SetUserAgent("DNSControl", version)
|
||||||
|
api.APILogin, api.APIPassword, api.APIEntity = conf["apilogin"], conf["apipassword"], conf["apientity"]
|
||||||
|
if conf["debugmode"] == "1" {
|
||||||
|
api.client.EnableDebugMode()
|
||||||
|
}
|
||||||
|
if api.APIEntity != "OTE" && api.APIEntity != "LIVE" {
|
||||||
|
return nil, fmt.Errorf("wrong api system entity used. use \"OTE\" for OT&E system or \"LIVE\" for Live system")
|
||||||
|
}
|
||||||
|
if api.APIEntity == "OTE" {
|
||||||
|
api.client.UseOTESystem()
|
||||||
|
}
|
||||||
|
if api.APILogin == "" || api.APIPassword == "" {
|
||||||
|
return nil, fmt.Errorf("missing login credentials apilogin or apipassword")
|
||||||
|
}
|
||||||
|
api.client.SetCredentials(api.APILogin, api.APIPassword)
|
||||||
|
return api, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReg(conf map[string]string) (providers.Registrar, error) {
|
||||||
|
return newProvider(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDsp(conf map[string]string, meta json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||||
|
return newProvider(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const providerName = "CNR"
|
||||||
|
const providerMaintainer = "@KaiSchwarz-cnic"
|
||||||
|
fns := providers.DspFuncs{
|
||||||
|
Initializer: newDsp,
|
||||||
|
RecordAuditor: AuditRecords,
|
||||||
|
}
|
||||||
|
providers.RegisterRegistrarType(providerName, newReg)
|
||||||
|
providers.RegisterDomainServiceProviderType(providerName, fns, features)
|
||||||
|
providers.RegisterMaintainer(providerName, providerMaintainer)
|
||||||
|
}
|
55
providers/cnr/domains.go
Normal file
55
providers/cnr/domains.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package cnr
|
||||||
|
|
||||||
|
// EnsureZoneExists returns an error
|
||||||
|
// * if access to dnszone is not allowed (not authorized) or
|
||||||
|
// * if it doesn't exist and creating it fails
|
||||||
|
func (n *CNRClient) EnsureZoneExists(domain string) error {
|
||||||
|
r := n.client.Request(map[string]interface{}{
|
||||||
|
"COMMAND": "StatusDNSZone",
|
||||||
|
"DNSZONE": domain,
|
||||||
|
})
|
||||||
|
code := r.GetCode()
|
||||||
|
if code == 545 {
|
||||||
|
command := map[string]interface{}{
|
||||||
|
"COMMAND": "AddDNSZone",
|
||||||
|
"DNSZONE": domain,
|
||||||
|
}
|
||||||
|
if n.APIEntity == "OTE" {
|
||||||
|
command["SOATTL"] = "33200"
|
||||||
|
command["SOASERIAL"] = "0000000000"
|
||||||
|
}
|
||||||
|
// Create the zone
|
||||||
|
r = n.client.Request(command)
|
||||||
|
if !r.IsSuccess() {
|
||||||
|
return n.GetCNRApiError("Failed to create not existing zone ", domain, r)
|
||||||
|
}
|
||||||
|
} else if code == 531 {
|
||||||
|
return n.GetCNRApiError("Not authorized to manage dnszone", domain, r)
|
||||||
|
} else if r.IsError() || r.IsTmpError() {
|
||||||
|
return n.GetCNRApiError("Error while checking status of dnszone", domain, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListZones lists all the
|
||||||
|
func (n *CNRClient) ListZones() ([]string, error) {
|
||||||
|
var zones []string
|
||||||
|
|
||||||
|
// Basic
|
||||||
|
|
||||||
|
rs := n.client.RequestAllResponsePages(map[string]string{
|
||||||
|
"COMMAND": "QueryDNSZoneList",
|
||||||
|
})
|
||||||
|
for _, r := range rs {
|
||||||
|
if r.IsError() {
|
||||||
|
return nil, n.GetCNRApiError("Error while QueryDNSZoneList", "Basic", &r)
|
||||||
|
}
|
||||||
|
zoneColumn := r.GetColumn("DNSZONE")
|
||||||
|
if zoneColumn != nil {
|
||||||
|
//return nil, fmt.Errorf("failed getting DNSZONE BASIC column")
|
||||||
|
zones = append(zones, zoneColumn.GetData()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return zones, nil
|
||||||
|
}
|
12
providers/cnr/error.go
Normal file
12
providers/cnr/error.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package cnr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCNRApiError returns an error including API error code and error description.
|
||||||
|
func (n *CNRClient) GetCNRApiError(format string, objectid string, r *response.Response) error {
|
||||||
|
return fmt.Errorf(format+" %q. [%v %s]", objectid, r.GetCode(), r.GetDescription())
|
||||||
|
}
|
106
providers/cnr/nameservers.go
Normal file
106
providers/cnr/nameservers.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package cnr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultNameservers = []*models.Nameserver{
|
||||||
|
{Name: "ns1.rrpproxy.net"},
|
||||||
|
{Name: "ns2.rrpproxy.net"},
|
||||||
|
{Name: "ns3.rrpproxy.net"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var nsRegex = regexp.MustCompile(`ns([1-3]{1})[0-9]+\.rrpproxy\.net`)
|
||||||
|
|
||||||
|
// GetNameservers gets the nameservers set on a domain.
|
||||||
|
func (n *CNRClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||||
|
// NOTE: This information is taken over from HX and adapted to CNR... might be wrong...
|
||||||
|
// This is an interesting edge case. CNR expects you to SET the nameservers to ns[1-3].rrpproxy.net,
|
||||||
|
// but it will internally set it to (ns1xyz|ns2uvw|ns3asd).rrpproxy.net, where xyz/uvw/asd is a uniqueish number.
|
||||||
|
// In order to avoid endless loops, we will use the unique nameservers if present, or else the generic ones if not.
|
||||||
|
nss, err := n.getNameserversRaw(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
toUse := []string{
|
||||||
|
defaultNameservers[0].Name,
|
||||||
|
defaultNameservers[1].Name,
|
||||||
|
defaultNameservers[2].Name,
|
||||||
|
}
|
||||||
|
for _, ns := range nss {
|
||||||
|
if matches := nsRegex.FindStringSubmatch(ns); len(matches) == 2 && len(matches[1]) == 1 {
|
||||||
|
idx := matches[1][0] - '1' // regex ensures proper range
|
||||||
|
toUse[idx] = matches[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models.ToNameservers(toUse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *CNRClient) getNameserversRaw(domain string) ([]string, error) {
|
||||||
|
r := n.client.Request(map[string]interface{}{
|
||||||
|
"COMMAND": "StatusDomain",
|
||||||
|
"DOMAIN": domain,
|
||||||
|
})
|
||||||
|
code := r.GetCode()
|
||||||
|
if code != 200 {
|
||||||
|
return nil, n.GetCNRApiError("Could not get status for domain", domain, r)
|
||||||
|
}
|
||||||
|
nsColumn := r.GetColumn("NAMESERVER")
|
||||||
|
if nsColumn == nil {
|
||||||
|
fmt.Println("No nameservers found")
|
||||||
|
return []string{}, nil // No nameserver assigned
|
||||||
|
}
|
||||||
|
ns := nsColumn.GetData()
|
||||||
|
sort.Strings(ns)
|
||||||
|
return ns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRegistrarCorrections gathers corrections that would being n to match dc.
|
||||||
|
func (n *CNRClient) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||||
|
nss, err := n.getNameserversRaw(dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
foundNameservers := strings.Join(nss, ",")
|
||||||
|
|
||||||
|
expected := []string{}
|
||||||
|
for _, ns := range dc.Nameservers {
|
||||||
|
name := strings.TrimRight(ns.Name, ".")
|
||||||
|
expected = append(expected, name)
|
||||||
|
}
|
||||||
|
sort.Strings(expected)
|
||||||
|
expectedNameservers := strings.Join(expected, ",")
|
||||||
|
|
||||||
|
if foundNameservers != expectedNameservers {
|
||||||
|
return []*models.Correction{
|
||||||
|
{
|
||||||
|
Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers),
|
||||||
|
F: n.updateNameservers(expected, dc.Name),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *CNRClient) updateNameservers(ns []string, domain string) func() error {
|
||||||
|
return func() error {
|
||||||
|
cmd := map[string]interface{}{
|
||||||
|
"COMMAND": "ModifyDomain",
|
||||||
|
"DOMAIN": domain,
|
||||||
|
}
|
||||||
|
for idx, ns := range ns {
|
||||||
|
cmd[fmt.Sprintf("NAMESERVER%d", idx)] = ns
|
||||||
|
}
|
||||||
|
response := n.client.Request(cmd)
|
||||||
|
code := response.GetCode()
|
||||||
|
if code != 200 {
|
||||||
|
return fmt.Errorf("%d %s", code, response.GetDescription())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
367
providers/cnr/records.go
Normal file
367
providers/cnr/records.go
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
package cnr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CNRRecord covers an individual DNS resource record.
|
||||||
|
type CNRRecord struct {
|
||||||
|
// DomainName is the zone that the record belongs to.
|
||||||
|
DomainName string
|
||||||
|
// Host is the hostname relative to the zone: e.g. for a record for blog.example.org, domain would be "example.org" and host would be "blog".
|
||||||
|
// An apex record would be specified by either an empty host "" or "@".
|
||||||
|
// A SRV record would be specified by "_{service}._{protocol}.{host}": e.g. "_sip._tcp.phone" for _sip._tcp.phone.example.org.
|
||||||
|
Host string
|
||||||
|
// FQDN is the Fully Qualified Domain Name. It is the combination of the host and the domain name. It always ends in a ".". FQDN is ignored in CreateRecord, specify via the Host field instead.
|
||||||
|
Fqdn string
|
||||||
|
// Type is one of the following: A, AAAA, ANAME, CNAME, MX, NS, SRV, or TXT.
|
||||||
|
Type string
|
||||||
|
// Answer is either the IP address for A or AAAA records; the target for ANAME, CNAME, MX, or NS records; the text for TXT records.
|
||||||
|
// For SRV records, answer has the following format: "{weight} {port} {target}" e.g. "1 5061 sip.example.org".
|
||||||
|
Answer string
|
||||||
|
// TTL is the time this record can be cached for in seconds.
|
||||||
|
TTL uint32
|
||||||
|
// Priority is only required for MX and SRV records, it is ignored for all others.
|
||||||
|
Priority uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||||
|
func (n *CNRClient) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
||||||
|
records, err := n.getRecords(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
actual := make([]*models.RecordConfig, len(records))
|
||||||
|
for i, r := range records {
|
||||||
|
actual[i] = toRecord(r, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return actual, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
||||||
|
func (n *CNRClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, int, error) {
|
||||||
|
toReport, create, del, mod, actualChangeCount, err := diff.NewCompat(dc).IncrementalDiff(actual)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
// Start corrections with the reports
|
||||||
|
corrections := diff.GenerateMessageCorrections(toReport)
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
// Print a list of changes. Generate an actual change that is the zone
|
||||||
|
changes := false
|
||||||
|
var builder strings.Builder
|
||||||
|
params := map[string]interface{}{}
|
||||||
|
delrridx := 0
|
||||||
|
addrridx := 0
|
||||||
|
|
||||||
|
for _, cre := range create {
|
||||||
|
changes = true
|
||||||
|
fmt.Fprintln(buf, cre)
|
||||||
|
newRecordString, err := n.createRecordString(cre.Desired, dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return corrections, 0, err
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("ADDRR%d", addrridx)
|
||||||
|
params[key] = newRecordString
|
||||||
|
fmt.Fprintf(&builder, "\033[32m+ %s = %s\033[0m\n", key, newRecordString)
|
||||||
|
addrridx++
|
||||||
|
}
|
||||||
|
for _, d := range del {
|
||||||
|
changes = true
|
||||||
|
fmt.Fprintln(buf, d)
|
||||||
|
key := fmt.Sprintf("DELRR%d", delrridx)
|
||||||
|
oldRecordString := n.deleteRecordString(d.Existing.Original.(*CNRRecord))
|
||||||
|
params[key] = oldRecordString
|
||||||
|
fmt.Fprintf(&builder, "\033[31m- %s = %s\033[0m\n", key, oldRecordString)
|
||||||
|
delrridx++
|
||||||
|
}
|
||||||
|
for _, chng := range mod {
|
||||||
|
changes = true
|
||||||
|
fmt.Fprintln(buf, chng)
|
||||||
|
// old record deletion
|
||||||
|
key := fmt.Sprintf("DELRR%d", delrridx)
|
||||||
|
oldRecordString := n.deleteRecordString(chng.Existing.Original.(*CNRRecord))
|
||||||
|
params[key] = oldRecordString
|
||||||
|
fmt.Fprintf(&builder, "\033[31m- %s = %s\033[0m\n", key, oldRecordString)
|
||||||
|
delrridx++
|
||||||
|
// new record creation
|
||||||
|
newRecordString, err := n.createRecordString(chng.Desired, dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return corrections, 0, err
|
||||||
|
}
|
||||||
|
key = fmt.Sprintf("ADDRR%d", addrridx)
|
||||||
|
params[key] = newRecordString
|
||||||
|
fmt.Fprintf(&builder, "\033[32m+ %s = %s\033[0m\n", key, newRecordString)
|
||||||
|
addrridx++
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes {
|
||||||
|
msg := fmt.Sprintf("GENERATE_ZONE: %s\n%s", dc.Name, buf.String())
|
||||||
|
if n.isDebugOn() {
|
||||||
|
msg = fmt.Sprintf("GENERATE_ZONE: %s\n%sPROVIDER CNR, API COMMAND PARAMETERS:\n%s", dc.Name, buf.String(), builder.String())
|
||||||
|
}
|
||||||
|
corrections = append(corrections, &models.Correction{
|
||||||
|
Msg: msg,
|
||||||
|
F: func() error {
|
||||||
|
return n.updateZoneBy(params, dc.Name)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return corrections, actualChangeCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRecord(r *CNRRecord, origin string) *models.RecordConfig {
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: r.Type,
|
||||||
|
TTL: r.TTL,
|
||||||
|
Original: r,
|
||||||
|
}
|
||||||
|
fqdn := r.Fqdn[:len(r.Fqdn)-1]
|
||||||
|
rc.SetLabelFromFQDN(fqdn, origin)
|
||||||
|
|
||||||
|
switch r.Type {
|
||||||
|
case "MX", "SRV":
|
||||||
|
if r.Priority > 65535 {
|
||||||
|
panic(fmt.Errorf("priority value out of range for %s record: %d", r.Type, r.Priority))
|
||||||
|
}
|
||||||
|
if r.Type == "MX" {
|
||||||
|
if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
|
||||||
|
panic(fmt.Errorf("unparsable MX record received from centralnic reseller API: %w", err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// _service._proto.name. TTL Type Priority Weight Port Target.
|
||||||
|
// e.g. _sip._tcp.phone.example.org. 86400 IN SRV 5 6 7 sip.example.org.
|
||||||
|
// r.Anser covers the format "Priority Weight Port Target" and we've to remove the priority from the string
|
||||||
|
r.Answer = strings.TrimPrefix(r.Answer, fmt.Sprintf("%d ", r.Priority))
|
||||||
|
if err := rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Answer); err != nil {
|
||||||
|
panic(fmt.Errorf("unparsable SRV record received from centralnic reseller API: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: // "A", "AAAA", "ANAME", "CNAME", "NS", "TXT", "CAA", "TLSA", "PTR"
|
||||||
|
if err := rc.PopulateFromStringFunc(r.Type, r.Answer, r.Fqdn, txtutil.ParseQuoted); err != nil {
|
||||||
|
panic(fmt.Errorf("unparsable record received from centralnic reseller API: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateZoneBy updates the zone with the provided changes.
|
||||||
|
func (n *CNRClient) updateZoneBy(params map[string]interface{}, domain string) error {
|
||||||
|
zone := domain
|
||||||
|
cmd := map[string]interface{}{
|
||||||
|
"COMMAND": "ModifyDNSZone",
|
||||||
|
"DNSZONE": zone,
|
||||||
|
}
|
||||||
|
for key, val := range params {
|
||||||
|
cmd[key] = val
|
||||||
|
}
|
||||||
|
r := n.client.Request(cmd)
|
||||||
|
if !r.IsSuccess() {
|
||||||
|
return n.GetCNRApiError("Error while updating zone", zone, r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteRecordString constructs the record string based on the provided CNRRecord.
|
||||||
|
func (n *CNRClient) getRecords(domain string) ([]*CNRRecord, error) {
|
||||||
|
var records []*CNRRecord
|
||||||
|
|
||||||
|
// Command to find out the total numbers of resource records for the zone
|
||||||
|
// so that the follow-up query can be done with the correct limit
|
||||||
|
cmd := map[string]interface{}{
|
||||||
|
"COMMAND": "QueryDNSZoneRRList",
|
||||||
|
"DNSZONE": domain,
|
||||||
|
"ORDERBY": "type",
|
||||||
|
"FIRST": "0",
|
||||||
|
"LIMIT": "1",
|
||||||
|
}
|
||||||
|
r := n.client.Request(cmd)
|
||||||
|
|
||||||
|
// Check if the request was successful
|
||||||
|
if !r.IsSuccess() {
|
||||||
|
if r.GetCode() == 545 {
|
||||||
|
// If dns zone does not exist create a new one automatically
|
||||||
|
if !isNoPopulate() {
|
||||||
|
n.EnsureZoneExists(domain)
|
||||||
|
} else {
|
||||||
|
// Return specific error if the zone does not exist
|
||||||
|
return nil, n.GetCNRApiError("Use `dnscontrol create-domains` to create not-existing zone", domain, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return general error for any other issues
|
||||||
|
return nil, n.GetCNRApiError("Failed loading resource records for zone", domain, r)
|
||||||
|
}
|
||||||
|
totalRecords := r.GetRecordsTotalCount()
|
||||||
|
if totalRecords <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
limitation := 100
|
||||||
|
totalRecords += limitation
|
||||||
|
|
||||||
|
// finally request all resource records available for the zone
|
||||||
|
cmd["LIMIT"] = fmt.Sprintf("%d", totalRecords)
|
||||||
|
cmd["WIDE"] = "1"
|
||||||
|
r = n.client.Request(cmd)
|
||||||
|
|
||||||
|
// Check if the request was successful
|
||||||
|
if !r.IsSuccess() {
|
||||||
|
// Return general error for any other issues
|
||||||
|
return nil, n.GetCNRApiError("Failed loading resource records for zone", domain, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop over the records array
|
||||||
|
rrs := r.GetRecords()
|
||||||
|
for i := 0; i < len(rrs); i++ {
|
||||||
|
data := rrs[i].GetData()
|
||||||
|
// fmt.Printf("Data: %+v\n", data)
|
||||||
|
if _, exists := data["NAME"]; !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if data["TYPE"] == "MX" {
|
||||||
|
tmp := strings.Split(data["CONTENT"], " ")
|
||||||
|
data["PRIO"] = tmp[0]
|
||||||
|
data["CONTENT"] = tmp[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the TTL string to an unsigned integer
|
||||||
|
ttl, err := strconv.ParseUint(data["TTL"], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid TTL value for domain %s: %s", domain, data["TTL"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the TTL string to an unsigned integer
|
||||||
|
priority, _ := strconv.ParseUint(data["PRIO"], 10, 32)
|
||||||
|
|
||||||
|
// Add dot to Answer if supported by the record type
|
||||||
|
pattern := `^CNAME|MX|NS|SRV|PTR$`
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error compiling regex in getRecords: %s", err)
|
||||||
|
}
|
||||||
|
if re.MatchString(data["TYPE"]) && !strings.HasSuffix(data["CONTENT"], ".") {
|
||||||
|
data["CONTENT"] = fmt.Sprintf("%s.", data["CONTENT"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only append domain if it's not already a fully qualified domain name
|
||||||
|
fqdn := fmt.Sprintf("%s.", domain)
|
||||||
|
if data["NAME"] != "@" && !strings.HasSuffix(data["NAME"], domain+".") {
|
||||||
|
fqdn = fmt.Sprintf("%s.%s.", data["NAME"], domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a new CNRRecord
|
||||||
|
record := &CNRRecord{
|
||||||
|
DomainName: domain,
|
||||||
|
Host: data["NAME"],
|
||||||
|
Fqdn: fqdn,
|
||||||
|
Type: data["TYPE"],
|
||||||
|
Answer: data["CONTENT"],
|
||||||
|
TTL: uint32(ttl),
|
||||||
|
Priority: uint32(priority),
|
||||||
|
}
|
||||||
|
// fmt.Printf("Record: %+v\n", record)
|
||||||
|
|
||||||
|
// Append the record to the records slice
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the slice of records
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create record string from given RecordConfig for the ADDRR# API parameter
|
||||||
|
func (n *CNRClient) createRecordString(rc *models.RecordConfig, domain string) (string, error) {
|
||||||
|
host := rc.GetLabel()
|
||||||
|
answer := ""
|
||||||
|
|
||||||
|
switch rc.Type { // #rtype_variations
|
||||||
|
case "A", "AAAA", "ANAME", "CNAME", "MX", "NS", "PTR":
|
||||||
|
answer = rc.GetTargetField()
|
||||||
|
if domain == host {
|
||||||
|
host = fmt.Sprintf(`%s.`, host)
|
||||||
|
}
|
||||||
|
case "TLSA":
|
||||||
|
answer = fmt.Sprintf(`%v %v %v %s`, rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType, rc.GetTargetField())
|
||||||
|
case "CAA":
|
||||||
|
answer = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
|
||||||
|
case "TXT":
|
||||||
|
answer = txtutil.EncodeQuoted(rc.GetTargetTXTJoined())
|
||||||
|
case "SRV":
|
||||||
|
if rc.GetTargetField() == "." {
|
||||||
|
return "", fmt.Errorf("SRV records with empty targets are not supported")
|
||||||
|
}
|
||||||
|
// _service._proto.name. TTL Type Priority Weight Port Target.
|
||||||
|
// e.g. _sip._tcp.phone.example.org. 86400 IN SRV 5 6 7 sip.example.org.
|
||||||
|
answer = fmt.Sprintf("%d %d %d %v", uint32(rc.SrvPriority), rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("createRecordString rtype %v unimplemented", rc.Type))
|
||||||
|
// We panic so that we quickly find any switch statements
|
||||||
|
// that have not been updated for a new RR type.
|
||||||
|
}
|
||||||
|
|
||||||
|
str := host + " " + fmt.Sprint(rc.TTL) + " "
|
||||||
|
|
||||||
|
if rc.Type != "NS" { // TODO
|
||||||
|
str += "IN "
|
||||||
|
}
|
||||||
|
str += rc.Type + " "
|
||||||
|
// Handle MX records which have priority
|
||||||
|
if rc.Type == "MX" {
|
||||||
|
str += fmt.Sprint(uint32(rc.MxPreference)) + " "
|
||||||
|
}
|
||||||
|
str += answer
|
||||||
|
return str, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteRecordString constructs the record string based on the provided CNRRecord.
|
||||||
|
func (n *CNRClient) deleteRecordString(record *CNRRecord) string {
|
||||||
|
// Initialize values slice
|
||||||
|
values := []string{
|
||||||
|
record.Host,
|
||||||
|
fmt.Sprintf("%v", record.TTL),
|
||||||
|
"IN",
|
||||||
|
record.Type,
|
||||||
|
}
|
||||||
|
if record.Type == "SRV" {
|
||||||
|
values = append(values, fmt.Sprintf("%d", record.Priority))
|
||||||
|
}
|
||||||
|
values = append(values, record.Answer)
|
||||||
|
|
||||||
|
// fmt.Printf("Values: %+v\n", values)
|
||||||
|
|
||||||
|
// Remove IN if the record type is "NS" TODO
|
||||||
|
if record.Type == "NS" {
|
||||||
|
values = append(values[:2], values[3:]...) // Skip over the "IN"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the final string by joining the elements with spaces
|
||||||
|
return strings.Join(values, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check the no-populate argument
|
||||||
|
func isNoPopulate() bool {
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if arg == "--no-populate" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check if debug mode is enabled
|
||||||
|
func (n *CNRClient) isDebugOn() bool {
|
||||||
|
return n.conf["debugmode"] == "1"
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue