Add DNS provider for Zonomi and RimuHosting. (#1084)

This commit is contained in:
Ludovic Fernandez 2020-03-14 13:32:50 +01:00 committed by GitHub
parent a5e60d5d05
commit 1aeac60ab3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1415 additions and 5 deletions

View file

@ -59,8 +59,9 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
| [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) |
| [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [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/) | | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [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/) | [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/) | | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [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/) | [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/) | | [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/) |
| [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) |
| [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) |
| [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | |
<!-- END DNS PROVIDERS LIST --> <!-- END DNS PROVIDERS LIST -->

View file

@ -69,6 +69,7 @@ func allDNSCodes() string {
"rackspace", "rackspace",
"regru", "regru",
"rfc2136", "rfc2136",
"rimuhosting",
"route53", "route53",
"sakuracloud", "sakuracloud",
"scaleway", "scaleway",
@ -81,6 +82,7 @@ func allDNSCodes() string {
"vscale", "vscale",
"vultr", "vultr",
"zoneee", "zoneee",
"zonomi",
} }
sort.Strings(providers) sort.Strings(providers)
return strings.Join(providers, ", ") return strings.Join(providers, ", ")
@ -1260,6 +1262,26 @@ func displayDNSHelp(name string) error {
ew.writeln() ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/rfc2136`) ew.writeln(`More information: https://go-acme.github.io/lego/dns/rfc2136`)
case "rimuhosting":
// generated from: providers/dns/rimuhosting/rimuhosting.toml
ew.writeln(`Configuration for RimuHosting.`)
ew.writeln(`Code: 'rimuhosting'`)
ew.writeln(`Since: 'v0.3.5'`)
ew.writeln()
ew.writeln(`Credentials:`)
ew.writeln(` - "RIMUHOSTING_API_KEY": User API key`)
ew.writeln()
ew.writeln(`Additional Configuration:`)
ew.writeln(` - "RIMUHOSTING_HTTP_TIMEOUT": API request timeout`)
ew.writeln(` - "RIMUHOSTING_POLLING_INTERVAL": Time between DNS propagation check`)
ew.writeln(` - "RIMUHOSTING_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
ew.writeln(` - "RIMUHOSTING_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/rimuhosting`)
case "route53": case "route53":
// generated from: providers/dns/route53/route53.toml // generated from: providers/dns/route53/route53.toml
ew.writeln(`Configuration for Amazon Route 53.`) ew.writeln(`Configuration for Amazon Route 53.`)
@ -1516,6 +1538,26 @@ func displayDNSHelp(name string) error {
ew.writeln() ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneee`) ew.writeln(`More information: https://go-acme.github.io/lego/dns/zoneee`)
case "zonomi":
// generated from: providers/dns/zonomi/zonomi.toml
ew.writeln(`Configuration for Zonomi.`)
ew.writeln(`Code: 'zonomi'`)
ew.writeln(`Since: 'v0.3.5'`)
ew.writeln()
ew.writeln(`Credentials:`)
ew.writeln(` - "ZONOMI_API_KEY": User API key`)
ew.writeln()
ew.writeln(`Additional Configuration:`)
ew.writeln(` - "ZONOMI_HTTP_TIMEOUT": API request timeout`)
ew.writeln(` - "ZONOMI_POLLING_INTERVAL": Time between DNS propagation check`)
ew.writeln(` - "ZONOMI_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
ew.writeln(` - "ZONOMI_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/zonomi`)
case "manual": case "manual":
ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`) ew.writeln(`Solving the DNS-01 challenge using CLI prompt.`)
default: default:

View file

@ -0,0 +1,62 @@
---
title: "RimuHosting"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: rimuhosting
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/rimuhosting/rimuhosting.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Since: v0.3.5
Configuration for [RimuHosting](https://rimuhosting.com).
<!--more-->
- Code: `rimuhosting`
Here is an example bash command using the RimuHosting provider:
```bash
RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
lego --dns rimuhosting --domains my.domain.com --email my@email.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `RIMUHOSTING_API_KEY` | User API key |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here](/lego/dns/#configuration-and-credentials).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `RIMUHOSTING_HTTP_TIMEOUT` | API request timeout |
| `RIMUHOSTING_POLLING_INTERVAL` | Time between DNS propagation check |
| `RIMUHOSTING_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `RIMUHOSTING_TTL` | The TTL of the TXT record used for the DNS challenge |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here](/lego/dns/#configuration-and-credentials).
## More information
- [API documentation](https://rimuhosting.com/dns/dyndns.jsp)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/rimuhosting/rimuhosting.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -0,0 +1,62 @@
---
title: "Zonomi"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: zonomi
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/zonomi/zonomi.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Since: v0.3.5
Configuration for [Zonomi](https://zonomi.com).
<!--more-->
- Code: `zonomi`
Here is an example bash command using the Zonomi provider:
```bash
ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
lego --dns zonomi --domains my.domain.com --email my@email.com run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `ZONOMI_API_KEY` | User API key |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here](/lego/dns/#configuration-and-credentials).
## Additional Configuration
| Environment Variable Name | Description |
|--------------------------------|-------------|
| `ZONOMI_HTTP_TIMEOUT` | API request timeout |
| `ZONOMI_POLLING_INTERVAL` | Time between DNS propagation check |
| `ZONOMI_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `ZONOMI_TTL` | The TTL of the TXT record used for the DNS challenge |
The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
More information [here](/lego/dns/#configuration-and-credentials).
## More information
- [API documentation](https://zonomi.com/app/dns/dyndns.jsp)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/zonomi/zonomi.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -60,6 +60,7 @@ import (
"github.com/go-acme/lego/v3/providers/dns/rackspace" "github.com/go-acme/lego/v3/providers/dns/rackspace"
"github.com/go-acme/lego/v3/providers/dns/regru" "github.com/go-acme/lego/v3/providers/dns/regru"
"github.com/go-acme/lego/v3/providers/dns/rfc2136" "github.com/go-acme/lego/v3/providers/dns/rfc2136"
"github.com/go-acme/lego/v3/providers/dns/rimuhosting"
"github.com/go-acme/lego/v3/providers/dns/route53" "github.com/go-acme/lego/v3/providers/dns/route53"
"github.com/go-acme/lego/v3/providers/dns/sakuracloud" "github.com/go-acme/lego/v3/providers/dns/sakuracloud"
"github.com/go-acme/lego/v3/providers/dns/scaleway" "github.com/go-acme/lego/v3/providers/dns/scaleway"
@ -72,6 +73,7 @@ import (
"github.com/go-acme/lego/v3/providers/dns/vscale" "github.com/go-acme/lego/v3/providers/dns/vscale"
"github.com/go-acme/lego/v3/providers/dns/vultr" "github.com/go-acme/lego/v3/providers/dns/vultr"
"github.com/go-acme/lego/v3/providers/dns/zoneee" "github.com/go-acme/lego/v3/providers/dns/zoneee"
"github.com/go-acme/lego/v3/providers/dns/zonomi"
) )
// NewDNSChallengeProviderByName Factory for DNS providers // NewDNSChallengeProviderByName Factory for DNS providers
@ -187,10 +189,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return rackspace.NewDNSProvider() return rackspace.NewDNSProvider()
case "regru": case "regru":
return regru.NewDNSProvider() return regru.NewDNSProvider()
case "route53":
return route53.NewDNSProvider()
case "rfc2136": case "rfc2136":
return rfc2136.NewDNSProvider() return rfc2136.NewDNSProvider()
case "rimuhosting":
return rimuhosting.NewDNSProvider()
case "route53":
return route53.NewDNSProvider()
case "sakuracloud": case "sakuracloud":
return sakuracloud.NewDNSProvider() return sakuracloud.NewDNSProvider()
case "scaleway": case "scaleway":
@ -213,6 +217,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return vscale.NewDNSProvider() return vscale.NewDNSProvider()
case "zoneee": case "zoneee":
return zoneee.NewDNSProvider() return zoneee.NewDNSProvider()
case "zonomi":
return zonomi.NewDNSProvider()
default: default:
return nil, fmt.Errorf("unrecognized DNS provider: %s", name) return nil, fmt.Errorf("unrecognized DNS provider: %s", name)
} }

View file

@ -0,0 +1,178 @@
package rimuhosting
import (
"encoding/xml"
"errors"
"io/ioutil"
"net/http"
"net/url"
"regexp"
querystring "github.com/google/go-querystring/query"
)
// Base URL for the RimuHosting DNS services.
const (
DefaultZonomiBaseURL = "https://zonomi.com/app/dns/dyndns.jsp"
DefaultRimuHostingBaseURL = "https://rimuhosting.com/app/dns/dyndns.jsp"
)
// Action names.
const (
SetAction = "SET"
QueryAction = "QUERY"
DeleteAction = "DELETE"
)
// Client the RimuHosting/Zonomi client.
type Client struct {
apiKey string
HTTPClient *http.Client
BaseURL string
}
// NewClient Creates a RimuHosting/Zonomi client.
func NewClient(apiKey string) *Client {
return &Client{
HTTPClient: http.DefaultClient,
BaseURL: DefaultZonomiBaseURL,
apiKey: apiKey,
}
}
// FindTXTRecords Finds TXT records.
// ex:
// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=example.com&api_key=apikeyvaluehere
// - https://zonomi.com/app/dns/dyndns.jsp?action=QUERY&name=**.example.com&api_key=apikeyvaluehere
func (c Client) FindTXTRecords(domain string) ([]Record, error) {
action := ActionParameter{
Action: QueryAction,
Name: domain,
Type: "TXT",
}
resp, err := c.DoActions(action)
if err != nil {
return nil, err
}
return resp.Actions.Action.Records, nil
}
// DoActions performs actions.
func (c Client) DoActions(actions ...ActionParameter) (*DNSAPIResult, error) {
if len(actions) == 0 {
return nil, errors.New("no action")
}
resp := &DNSAPIResult{}
if len(actions) == 1 {
action := actionParameter{
ActionParameter: actions[0],
APIKey: c.apiKey,
}
err := c.do(action, resp)
if err != nil {
return nil, err
}
return resp, nil
}
multi := c.toMultiParameters(actions)
err := c.do(multi, resp)
if err != nil {
return nil, err
}
return resp, nil
}
func (c Client) toMultiParameters(params []ActionParameter) multiActionParameter {
multi := multiActionParameter{
APIKey: c.apiKey,
}
for _, parameters := range params {
multi.Action = append(multi.Action, parameters.Action)
multi.Name = append(multi.Name, parameters.Name)
multi.Type = append(multi.Type, parameters.Type)
multi.Value = append(multi.Value, parameters.Value)
multi.TTL = append(multi.TTL, parameters.TTL)
}
return multi
}
func (c Client) do(params interface{}, data interface{}) error {
baseURL, err := url.Parse(c.BaseURL)
if err != nil {
return err
}
v, err := querystring.Values(params)
if err != nil {
return err
}
exp := regexp.MustCompile(`(%5B)(%5D)(\d+)=`)
baseURL.RawQuery = exp.ReplaceAllString(v.Encode(), "${1}${3}${2}=")
req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil)
if err != nil {
return err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
all, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode/100 != 2 {
r := APIError{}
err = xml.Unmarshal(all, &r)
if err != nil {
return err
}
return r
}
if data != nil {
err := xml.Unmarshal(all, data)
if err != nil {
return err
}
}
return nil
}
// AddRecord helper to create an action to add a TXT record.
func AddRecord(domain string, content string, ttl int) ActionParameter {
return ActionParameter{
Action: SetAction,
Name: domain,
Type: "TXT",
Value: content,
TTL: ttl,
}
}
// DeleteRecord helper to create an action to delete a TXT record.
func DeleteRecord(domain string, content string) ActionParameter {
return ActionParameter{
Action: DeleteAction,
Name: domain,
Type: "TXT",
Value: content,
}
}

View file

@ -0,0 +1,303 @@
package rimuhosting
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClient_FindTXTRecords(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
query := req.URL.Query()
var fixture string
switch query.Get("name") {
case "example.com":
fixture = "./fixtures/find_records.xml"
case "**.example.com":
fixture = "./fixtures/find_records_pattern.xml"
default:
fixture = "./fixtures/find_records_empty.xml"
}
err := writeResponse(rw, fixture)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
})
client := NewClient("apikeyvaluehere")
client.BaseURL = server.URL
testCases := []struct {
desc string
domain string
expected []Record
}{
{
desc: "simple",
domain: "example.com",
expected: []Record{
{
Name: "example.org",
Type: "TXT",
Content: "txttxtx",
TTL: "3600 seconds",
Priority: "0",
},
},
},
{
desc: "pattern",
domain: "**.example.com",
expected: []Record{
{
Name: "_test.example.org",
Type: "TXT",
Content: "txttxtx",
TTL: "3600 seconds",
Priority: "0",
},
{
Name: "example.org",
Type: "TXT",
Content: "txttxtx",
TTL: "3600 seconds",
Priority: "0",
},
},
},
{
desc: "empty",
domain: "empty.com",
expected: nil,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
records, err := client.FindTXTRecords(test.domain)
require.NoError(t, err)
assert.Equal(t, test.expected, records)
})
}
}
func TestClient_DoActions(t *testing.T) {
type expected struct {
Query string
Resp *DNSAPIResult
Error string
}
testCases := []struct {
desc string
actions []ActionParameter
fixture string
expected expected
}{
{
desc: "SET error",
actions: []ActionParameter{
AddRecord("example.com", "txttxtx", 0),
},
fixture: "./fixtures/add_record_error.xml",
expected: expected{
Query: "action=SET&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx",
Error: "ERROR: No zone found for example.com",
},
},
{
desc: "SET simple",
actions: []ActionParameter{
AddRecord("example.org", "txttxtx", 0),
},
fixture: "./fixtures/add_record.xml",
expected: expected{
Query: "action=SET&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx",
Resp: &DNSAPIResult{
XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
IsOk: "OK:",
ResultCounts: ResultCounts{Added: "1", Changed: "0", Unchanged: "0", Deleted: "0"},
Actions: Actions{
Action: Action{
Action: "SET",
Host: "example.org",
Type: "TXT",
Records: []Record{{
Name: "example.org",
Type: "TXT",
Content: "txttxtx",
TTL: "3600 seconds",
Priority: "0",
}},
}},
},
},
},
{
desc: "SET multiple values",
actions: []ActionParameter{
AddRecord("example.org", "txttxtx", 0),
AddRecord("example.org", "sample", 0),
},
fixture: "./fixtures/add_record_same_domain.xml",
expected: expected{
Query: "action[0]=SET&action[1]=SET&api_key=apikeyvaluehere&name[0]=example.org&name[1]=example.org&ttl[0]=0&ttl[1]=0&type[0]=TXT&type[1]=TXT&value[0]=txttxtx&value[1]=sample",
Resp: &DNSAPIResult{
XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
IsOk: "OK:",
ResultCounts: ResultCounts{Added: "2", Changed: "0", Unchanged: "0", Deleted: "0"},
Actions: Actions{
Action: Action{
Action: "SET",
Host: "example.org",
Type: "TXT",
Records: []Record{
{
Name: "example.org",
Type: "TXT",
Content: "txttxtx",
TTL: "0 seconds",
Priority: "0",
},
{
Name: "example.org",
Type: "TXT",
Content: "sample",
TTL: "0 seconds",
Priority: "0",
},
},
}}},
},
},
{
desc: "DELETE error",
actions: []ActionParameter{
DeleteRecord("example.com", "txttxtx"),
},
fixture: "./fixtures/delete_record_error.xml",
expected: expected{
Query: "action=DELETE&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx",
Error: "ERROR: No zone found for example.com",
},
},
{
desc: "DELETE nothing",
actions: []ActionParameter{
DeleteRecord("example.org", "nothing"),
},
fixture: "./fixtures/delete_record_nothing.xml",
expected: expected{
Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=nothing",
Resp: &DNSAPIResult{
XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
IsOk: "OK:",
ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "0"},
Actions: Actions{
Action: Action{
Action: "DELETE",
Host: "example.org",
Type: "TXT",
Records: nil,
}}},
},
},
{
desc: "DELETE simple",
actions: []ActionParameter{
DeleteRecord("example.org", "txttxtx"),
},
fixture: "./fixtures/delete_record.xml",
expected: expected{
Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx",
Resp: &DNSAPIResult{
XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
IsOk: "OK:",
ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "1"},
Actions: Actions{
Action: Action{
Action: "DELETE",
Host: "example.org",
Type: "TXT",
Records: []Record{{
Name: "example.org",
Type: "TXT",
Content: "txttxtx",
TTL: "3600 seconds",
Priority: "0",
}},
}}},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
query, err := url.QueryUnescape(req.URL.RawQuery)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
if test.expected.Query != query {
http.Error(rw, fmt.Sprintf("invalid query: %s", query), http.StatusBadRequest)
return
}
if test.expected.Error != "" {
rw.WriteHeader(http.StatusInternalServerError)
}
err = writeResponse(rw, test.fixture)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
})
client := NewClient("apikeyvaluehere")
client.BaseURL = server.URL
resp, err := client.DoActions(test.actions...)
if test.expected.Error != "" {
require.EqualError(t, err, test.expected.Error)
return
}
require.NoError(t, err)
assert.Equal(t, test.expected.Resp, resp)
})
}
}
func writeResponse(rw io.Writer, filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
_, err = io.Copy(rw, file)
return err
}

View file

@ -0,0 +1,20 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>
<result_counts
added="1"
changed="0"
unchanged="0"
deleted="0"/>
<actions>
<action
action="SET"
host="example.org"
type="TXT"
value="txttxtx">
<record
name="example.org"
type="TXT"
content="txttxtx"
ttl="3600 seconds"
prio="0"
added=""/> </action></actions></dnsapi_result>

View file

@ -0,0 +1,2 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]>
<error>ERROR: No zone found for example.com</error>

View file

@ -0,0 +1,34 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>
<result_counts
added="2"
changed="0"
unchanged="0"
deleted="0"/>
<actions>
<action
action="SET"
host="example.org"
type="TXT"
value="txttxtx"
ttl="0">
<record
name="example.org"
type="TXT"
content="txttxtx"
ttl="0 seconds"
prio="0"
added=""/> </action>
<action
action="SET"
host="example.org"
type="TXT"
value="sample"
ttl="0">
<record
name="example.org"
type="TXT"
content="sample"
ttl="0 seconds"
prio="0"
added=""/> </action></actions></dnsapi_result>

View file

@ -0,0 +1,20 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>
<result_counts
added="0"
changed="0"
unchanged="0"
deleted="1"/>
<actions>
<action
action="DELETE"
host="example.org"
type="TXT"
value="txttxtx">
<record
name="example.org"
type="TXT"
content="txttxtx"
ttl="3600 seconds"
prio="0"
deleted=""/> </action></actions></dnsapi_result>

View file

@ -0,0 +1,2 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]>
<error>ERROR: No zone found for example.com</error>

View file

@ -0,0 +1,13 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>
<result_counts
added="0"
changed="0"
unchanged="0"
deleted="0"/>
<actions>
<action
action="DELETE"
host="example.org"
type="TXT"
value="aaaa"></action></actions></dnsapi_result>

View file

@ -0,0 +1,18 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>
<result_counts
added="0"
changed="0"
unchanged="0"
deleted="0"/>
<actions>
<action
action="QUERY"
host="example.org"
type="TXT">
<record
name="example.org"
type="TXT"
content="txttxtx"
ttl="3600 seconds"
prio="0"/> </action></actions></dnsapi_result>

View file

@ -0,0 +1,12 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>
<result_counts
added="0"
changed="0"
unchanged="0"
deleted="0"/>
<actions>
<action
action="QUERY"
host="example.org"
type="TXT"></action></actions></dnsapi_result>

View file

@ -0,0 +1,24 @@
<?xml version ="1.0" ?><!DOCTYPE html [<!ENTITY nbsp '&#160;'><!ENTITY trade '&#8482;'><!ENTITY copy '&#169;'>]><dnsapi_result><is_ok>OK:</is_ok>
<result_counts
added="0"
changed="0"
unchanged="0"
deleted="0"/>
<actions>
<action
action="QUERY"
host="_test.example.org, example.org"
type="TXT">
<record
name="_test.example.org"
type="TXT"
content="txttxtx"
ttl="3600 seconds"
prio="0"/>
<record
name="example.org"
type="TXT"
content="txttxtx"
ttl="3600 seconds"
prio="0"/> </action></actions></dnsapi_result>

View file

@ -0,0 +1,71 @@
package rimuhosting
import "encoding/xml"
type ActionParameter struct {
Action string `url:"action,omitempty"`
Name string `url:"name,omitempty"`
Type string `url:"type,omitempty"`
Value string `url:"value,omitempty"`
TTL int `url:"ttl,omitempty"`
Priority int `url:"prio,omitempty"`
}
type actionParameter struct {
ActionParameter
APIKey string `url:"api_key,omitempty"`
}
type multiActionParameter struct {
APIKey string `url:"api_key,omitempty"`
Action []string `url:"action,brackets,numbered,omitempty"`
Name []string `url:"name,brackets,numbered,omitempty"`
Type []string `url:"type,brackets,numbered,omitempty"`
Value []string `url:"value,brackets,numbered,omitempty"`
TTL []int `url:"ttl,brackets,numbered,omitempty"`
Priority []int `url:"prio,brackets,numbered,omitempty"`
}
type APIError struct {
XMLName xml.Name `xml:"error"`
Text string `xml:",chardata"`
}
func (a APIError) Error() string {
return a.Text
}
type DNSAPIResult struct {
XMLName xml.Name `xml:"dnsapi_result"`
IsOk string `xml:"is_ok"`
ResultCounts ResultCounts `xml:"result_counts"`
Actions Actions `xml:"actions"`
}
type ResultCounts struct {
Added string `xml:"added,attr"`
Changed string `xml:"changed,attr"`
Unchanged string `xml:"unchanged,attr"`
Deleted string `xml:"deleted,attr"`
}
type Actions struct {
Action Action `xml:"action"`
}
type Action struct {
Action string `xml:"action,attr"`
Host string `xml:"host,attr"`
Type string `xml:"type,attr"`
Records []Record `xml:"record"`
}
type Record struct {
Name string `xml:"name,attr"`
Type string `xml:"type,attr"`
Content string `xml:"content,attr"`
TTL string `xml:"ttl,attr"`
Priority string `xml:"prio,attr"`
}

View file

@ -0,0 +1,128 @@
// Package rimuhosting implements a DNS provider for solving the DNS-01 challenge using RimuHosting DNS.
package rimuhosting
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/v3/challenge/dns01"
"github.com/go-acme/lego/v3/platform/config/env"
"github.com/go-acme/lego/v3/providers/dns/internal/rimuhosting"
)
// Environment variables names.
const (
envNamespace = "RIMUHOSTING_"
EnvAPIKey = envNamespace + "API_KEY"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 3600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
}
}
// DNSProvider is an implementation of the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *rimuhosting.Client
}
// NewDNSProvider returns a DNSProvider instance configured for RimuHosting.
// Credentials must be passed in the environment variables.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAPIKey)
if err != nil {
return nil, fmt.Errorf("rimuhosting: %w", err)
}
config := NewDefaultConfig()
config.APIKey = values[EnvAPIKey]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for RimuHosting.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("rimuhosting: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, errors.New("rimuhosting: incomplete credentials, missing API key")
}
client := rimuhosting.NewClient(config.APIKey)
client.BaseURL = rimuhosting.DefaultRimuHostingBaseURL
return &DNSProvider{config: config, client: client}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
records, err := d.client.FindTXTRecords(dns01.UnFqdn(fqdn))
if err != nil {
return fmt.Errorf("rimuhosting: failed to find record(s) for %s: %w", domain, err)
}
actions := []rimuhosting.ActionParameter{
rimuhosting.AddRecord(dns01.UnFqdn(fqdn), value, d.config.TTL),
}
for _, record := range records {
actions = append(actions, rimuhosting.AddRecord(record.Name, record.Content, d.config.TTL))
}
_, err = d.client.DoActions(actions...)
if err != nil {
return fmt.Errorf("rimuhosting: failed to add record(s) for %s: %w", domain, err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
action := rimuhosting.DeleteRecord(dns01.UnFqdn(fqdn), value)
_, err := d.client.DoActions(action)
if err != nil {
return fmt.Errorf("rimuhosting: failed to delete record for %s: %w", domain, err)
}
return nil
}

View file

@ -0,0 +1,22 @@
Name = "RimuHosting"
Description = ''''''
URL = "https://rimuhosting.com"
Code = "rimuhosting"
Since = "v0.3.5"
Example = '''
RIMUHOSTING_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
lego --dns rimuhosting --domains my.domain.com --email my@email.com run
'''
[Configuration]
[Configuration.Credentials]
RIMUHOSTING_API_KEY = "User API key"
[Configuration.Additional]
RIMUHOSTING_POLLING_INTERVAL = "Time between DNS propagation check"
RIMUHOSTING_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
RIMUHOSTING_TTL = "The TTL of the TXT record used for the DNS challenge"
RIMUHOSTING_HTTP_TIMEOUT = "API request timeout"
[Links]
API = "https://rimuhosting.com/dns/dyndns.jsp"

View file

@ -0,0 +1,120 @@
package rimuhosting
import (
"testing"
"time"
"github.com/go-acme/lego/v3/platform/tester"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvAPIKey: "123",
},
},
{
desc: "missing api key",
envVars: map[string]string{
EnvAPIKey: "",
},
expected: "rimuhosting: some credentials information are missing: RIMUHOSTING_API_KEY",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
envTest.Apply(test.envVars)
p, err := NewDNSProvider()
if len(test.expected) == 0 {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
expected string
apiKey string
secretKey string
}{
{
desc: "success",
apiKey: "api_key",
secretKey: "api_secret",
},
{
desc: "missing api key",
apiKey: "",
secretKey: "api_secret",
expected: "rimuhosting: incomplete credentials, missing API key",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.APIKey = test.apiKey
p, err := NewDNSProviderConfig(config)
if len(test.expected) == 0 {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.Present(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func TestLiveCleanUp(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
time.Sleep(1 * time.Second)
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}

View file

@ -0,0 +1,128 @@
// Package zonomi implements a DNS provider for solving the DNS-01 challenge using Zonomi DNS.
package zonomi
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/go-acme/lego/v3/challenge/dns01"
"github.com/go-acme/lego/v3/platform/config/env"
"github.com/go-acme/lego/v3/providers/dns/internal/rimuhosting"
)
// Environment variables names.
const (
envNamespace = "ZONOMI_"
EnvAPIKey = envNamespace + "API_KEY"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
// Config is used to configure the creation of the DNSProvider
type Config struct {
APIKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 3600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
},
}
}
// DNSProvider is an implementation of the challenge.Provider interface.
type DNSProvider struct {
config *Config
client *rimuhosting.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Zonomi.
// Credentials must be passed in the environment variables.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAPIKey)
if err != nil {
return nil, fmt.Errorf("zonomi: %w", err)
}
config := NewDefaultConfig()
config.APIKey = values[EnvAPIKey]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for Zonomi.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("zonomi: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
return nil, errors.New("zonomi: incomplete credentials, missing API key")
}
client := rimuhosting.NewClient(config.APIKey)
client.BaseURL = rimuhosting.DefaultZonomiBaseURL
return &DNSProvider{config: config, client: client}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
records, err := d.client.FindTXTRecords(dns01.UnFqdn(fqdn))
if err != nil {
return fmt.Errorf("zonomi: failed to find record(s) for %s: %w", domain, err)
}
actions := []rimuhosting.ActionParameter{
rimuhosting.AddRecord(dns01.UnFqdn(fqdn), value, d.config.TTL),
}
for _, record := range records {
actions = append(actions, rimuhosting.AddRecord(record.Name, record.Content, d.config.TTL))
}
_, err = d.client.DoActions(actions...)
if err != nil {
return fmt.Errorf("zonomi: failed to add record(s) for %s: %w", domain, err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
action := rimuhosting.DeleteRecord(dns01.UnFqdn(fqdn), value)
_, err := d.client.DoActions(action)
if err != nil {
return fmt.Errorf("zonomi: failed to delete record for %s: %w", domain, err)
}
return nil
}

View file

@ -0,0 +1,22 @@
Name = "Zonomi"
Description = ''''''
URL = "https://zonomi.com"
Code = "zonomi"
Since = "v0.3.5"
Example = '''
ZONOMI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
lego --dns zonomi --domains my.domain.com --email my@email.com run
'''
[Configuration]
[Configuration.Credentials]
ZONOMI_API_KEY = "User API key"
[Configuration.Additional]
ZONOMI_POLLING_INTERVAL = "Time between DNS propagation check"
ZONOMI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
ZONOMI_TTL = "The TTL of the TXT record used for the DNS challenge"
ZONOMI_HTTP_TIMEOUT = "API request timeout"
[Links]
API = "https://zonomi.com/app/dns/dyndns.jsp"

View file

@ -0,0 +1,120 @@
package zonomi
import (
"testing"
"time"
"github.com/go-acme/lego/v3/platform/tester"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvAPIKey: "123",
},
},
{
desc: "missing api key",
envVars: map[string]string{
EnvAPIKey: "",
},
expected: "zonomi: some credentials information are missing: ZONOMI_API_KEY",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
envTest.Apply(test.envVars)
p, err := NewDNSProvider()
if len(test.expected) == 0 {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
expected string
apiKey string
secretKey string
}{
{
desc: "success",
apiKey: "api_key",
secretKey: "api_secret",
},
{
desc: "missing api key",
apiKey: "",
secretKey: "api_secret",
expected: "zonomi: incomplete credentials, missing API key",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.APIKey = test.apiKey
p, err := NewDNSProviderConfig(config)
if len(test.expected) == 0 {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.Present(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func TestLiveCleanUp(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
time.Sleep(1 * time.Second)
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}