Skip to content

Commit

Permalink
Add DNS provider for RU CENTER (go-acme#1891)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonDzyk authored and ldez committed May 5, 2023
1 parent 67d80a1 commit 697f4f4
Show file tree
Hide file tree
Showing 9 changed files with 1,064 additions and 8 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
| [Nodion](https://go-acme.github.io/lego/dns/nodion/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) |
| [OVH](https://go-acme.github.io/lego/dns/ovh/) | [plesk.com](https://go-acme.github.io/lego/dns/plesk/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) |
| [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) |
| [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) |
| [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) |
| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) |
| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) |
| [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Websupport](https://go-acme.github.io/lego/dns/websupport/) |
| [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) |
| [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | | |
| [RU CENTER](https://go-acme.github.io/lego/dns/nicru/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) |
| [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) |
| [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) |
| [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) |
| [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) |
| [Websupport](https://go-acme.github.io/lego/dns/websupport/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/) |
| [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | |

<!-- END DNS PROVIDERS LIST -->

Expand Down
25 changes: 25 additions & 0 deletions cmd/zz_gen_cmd_dnshelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func allDNSCodes() string {
"netcup",
"netlify",
"nicmanager",
"nicru",
"nifcloud",
"njalla",
"nodion",
Expand Down Expand Up @@ -1740,6 +1741,30 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/nicmanager`)

case "nicru":
// generated from: providers/dns/nicru/nicru.toml
ew.writeln(`Configuration for RU CENTER.`)
ew.writeln(`Code: 'nicru'`)
ew.writeln(`Since: 'v4.11.0'`)
ew.writeln()

ew.writeln(`Credentials:`)
ew.writeln(` - "NIC_RU_PASSWORD": Password for account in RU CENTER`)
ew.writeln(` - "NIC_RU_SECRET": Secret for application in DNS-hosting RU CENTER`)
ew.writeln(` - "NIC_RU_SERVICE_ID": Service ID for application in DNS-hosting RU CENTER`)
ew.writeln(` - "NIC_RU_SERVICE_NAME": Service Name for DNS-hosting RU CENTER`)
ew.writeln(` - "NIC_RU_USER": Agreement for account in RU CENTER`)
ew.writeln()

ew.writeln(`Additional Configuration:`)
ew.writeln(` - "NIC_RU_HTTP_TIMEOUT": API request timeout`)
ew.writeln(` - "NIC_RU_POLLING_INTERVAL": Time between DNS propagation check`)
ew.writeln(` - "NIC_RU_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
ew.writeln(` - "NIC_RU_TTL": The TTL of the TXT record used for the DNS challenge`)

ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/nicru`)

case "nifcloud":
// generated from: providers/dns/nifcloud/nifcloud.toml
ew.writeln(`Configuration for NIFCloud.`)
Expand Down
2 changes: 1 addition & 1 deletion docs/data/zz_cli_help.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudxns, conoha, constellix, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, websupport, wedos, yandex, yandexcloud, zoneee, zonomi
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudxns, conoha, constellix, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nicru, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, websupport, wedos, yandex, yandexcloud, zoneee, zonomi
More information: https://go-acme.github.io/lego/dns
"""
3 changes: 3 additions & 0 deletions providers/dns/dns_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/netcup"
"github.com/go-acme/lego/v4/providers/dns/netlify"
"github.com/go-acme/lego/v4/providers/dns/nicmanager"
"github.com/go-acme/lego/v4/providers/dns/nicru"
"github.com/go-acme/lego/v4/providers/dns/nifcloud"
"github.com/go-acme/lego/v4/providers/dns/njalla"
"github.com/go-acme/lego/v4/providers/dns/nodion"
Expand Down Expand Up @@ -280,6 +281,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return netlify.NewDNSProvider()
case "nicmanager":
return nicmanager.NewDNSProvider()
case "nicru":
return nicru.NewDNSProvider()
case "nifcloud":
return nifcloud.NewDNSProvider()
case "njalla":
Expand Down
297 changes: 297 additions & 0 deletions providers/dns/nicru/internal/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
package internal

import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"golang.org/x/oauth2"
"net/http"
"strconv"
)

const (
BaseURL = `https://api.nic.ru`
TokenURL = BaseURL + `/oauth/token`
GetZonesUrlPattern = BaseURL + `/dns-master/services/%s/zones`
GetRecordsUrlPattern = BaseURL + `/dns-master/services/%s/zones/%s/records`
DeleteRecordsUrlPattern = BaseURL + `/dns-master/services/%s/zones/%s/records/%d`
AddRecordsUrlPattern = BaseURL + `/dns-master/services/%s/zones/%s/records`
CommitUrlPattern = BaseURL + `/dns-master/services/%s/zones/%s/commit`
SuccessStatus = `success`
OAuth2Scope = `.+:/dns-master/.+`
)

// Provider facilitates DNS record manipulation with NIC.ru.
type Provider struct {
OAuth2ClientID string `json:"oauth2_client_id"`
OAuth2SecretID string `json:"oauth2_secret_id"`
Username string `json:"username"`
Password string `json:"password"`
ServiceName string `json:"service_name"`
}

type Client struct {
client *http.Client
provider *Provider
token string
}

func NewClient(provider *Provider) (*Client, error) {
client := Client{provider: provider}
err := client.validateAuthOptions()
if err != nil {
return nil, err
}
return &client, nil
}

func (client *Client) GetOauth2Client() error {
ctx := context.TODO()

oauth2Config := oauth2.Config{
ClientID: client.provider.OAuth2ClientID,
ClientSecret: client.provider.OAuth2SecretID,
Endpoint: oauth2.Endpoint{
TokenURL: TokenURL,
AuthStyle: oauth2.AuthStyleInParams,
},
Scopes: []string{OAuth2Scope},
}

oauth2Token, err := oauth2Config.PasswordCredentialsToken(ctx, client.provider.Username, client.provider.Password)
if err != nil {
return fmt.Errorf("nicru: %s", err.Error())
}

client.client = oauth2Config.Client(ctx, oauth2Token)
return nil
}

func (client *Client) Do(r *http.Request) (*http.Response, error) {
if client.client == nil {
err := client.GetOauth2Client()
if err != nil {
return nil, err
}
}
return client.client.Do(r)
}

func (client *Client) GetZones() ([]*Zone, error) {
request, err := http.NewRequest(http.MethodGet, fmt.Sprintf(GetZonesUrlPattern, client.provider.ServiceName), nil)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}

buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(response.Body); err != nil {
return nil, err
}

apiResponse := &Response{}
if err := xml.NewDecoder(buf).Decode(&apiResponse); err != nil {
return nil, err
} else {
var zones []*Zone
for _, zone := range apiResponse.Data.Zone {
zones = append(zones, zone)
}
return zones, nil
}
}

func (client *Client) GetRecords(fqdn string) ([]*RR, error) {
request, err := http.NewRequest(
http.MethodGet,
fmt.Sprintf(GetRecordsUrlPattern, client.provider.ServiceName, fqdn),
nil)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}

buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(response.Body); err != nil {
return nil, err
}

apiResponse := &Response{}
if err := xml.NewDecoder(buf).Decode(&apiResponse); err != nil {
return nil, err
} else {
var records []*RR
for _, zone := range apiResponse.Data.Zone {
records = append(records, zone.Rr...)
}
return records, nil
}
}

func (client *Client) add(zoneName string, request *Request) (*Response, error) {

buf := bytes.NewBuffer(nil)
if err := xml.NewEncoder(buf).Encode(request); err != nil {
return nil, err
}

url := fmt.Sprintf(AddRecordsUrlPattern, client.provider.ServiceName, zoneName)

req, err := http.NewRequest(http.MethodPut, url, buf)
if err != nil {
return nil, err
}

response, err := client.Do(req)
if err != nil {
return nil, err
}

buf = bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(response.Body); err != nil {
return nil, err
}

apiResponse := &Response{}
if err := xml.NewDecoder(buf).Decode(&apiResponse); err != nil {
return nil, err
}

if apiResponse.Status != SuccessStatus {
return nil, fmt.Errorf(describeError(apiResponse.Errors.Error))
} else {
return apiResponse, nil
}
}

func (client *Client) deleteRecord(zoneName string, id int) (*Response, error) {
url := fmt.Sprintf(DeleteRecordsUrlPattern, client.provider.ServiceName, zoneName, id)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return nil, err
}
response, err := client.Do(req)
if err != nil {
return nil, err
}
apiResponse := Response{}
if err := xml.NewDecoder(response.Body).Decode(&apiResponse); err != nil {
return nil, err
}
if apiResponse.Status != SuccessStatus {
return nil, err
} else {
return &apiResponse, nil
}
}

func (client *Client) GetTXTRecords(fqdn string) ([]*Txt, error) {
records, err := client.GetRecords(fqdn)
if err != nil {
return nil, err
}

txt := make([]*Txt, 0)
for _, record := range records {
if record.Txt != nil {
txt = append(txt, record.Txt)
}
}

return txt, nil
}

func (client *Client) AddTxtRecord(zoneName string, name string, content string, ttl int) (*Response, error) {
request := &Request{
RrList: &RrList{
Rr: []*RR{},
},
}
request.RrList.Rr = append(request.RrList.Rr, &RR{
Name: name,
Ttl: strconv.Itoa(ttl),
Type: `TXT`,
Txt: &Txt{
String: content,
},
})

return client.add(zoneName, request)
}

func (client *Client) DeleteRecord(zoneName string, id int) (*Response, error) {
url := fmt.Sprintf(DeleteRecordsUrlPattern, client.provider.ServiceName, zoneName, id)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return nil, err
}
response, err := client.Do(req)
if err != nil {
return nil, err
}
apiResponse := Response{}
if err := xml.NewDecoder(response.Body).Decode(&apiResponse); err != nil {
return nil, err
}
if apiResponse.Status != SuccessStatus {
return nil, err
} else {
return &apiResponse, nil
}
}

func (client *Client) CommitZone(zoneName string) (*Response, error) {
url := fmt.Sprintf(CommitUrlPattern, client.provider.ServiceName, zoneName)
request, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
apiResponse := Response{}
if err := xml.NewDecoder(response.Body).Decode(&apiResponse); err != nil {
return nil, err
}
if apiResponse.Status != SuccessStatus {
return nil, err
} else {
return &apiResponse, nil
}
}

func (client *Client) validateAuthOptions() error {

msg := " is missing in credentials information"

if client.provider.ServiceName == "" {
return errors.New("service name" + msg)
}

if client.provider.Username == "" {
return errors.New("username" + msg)
}

if client.provider.Password == "" {
return errors.New("password" + msg)
}

if client.provider.OAuth2ClientID == "" {
return errors.New("serviceId" + msg)
}

if client.provider.OAuth2SecretID == "" {
return errors.New("secret" + msg)
}

return nil
}
Loading

0 comments on commit 697f4f4

Please sign in to comment.