mirror of
https://github.com/go-acme/lego.git
synced 2024-12-26 03:09:37 +02:00
Add DNS provider for nicmanager (#1473)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
parent
376e7bd78e
commit
d2e526e8dd
16
README.md
16
README.md
@ -62,14 +62,14 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
|
||||
| [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) |
|
||||
| [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) |
|
||||
| [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [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/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [Njalla](https://go-acme.github.io/lego/dns/njalla/) |
|
||||
| [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/) |
|
||||
| [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/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) |
|
||||
| [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) |
|
||||
| [Yandex](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/) | |
|
||||
| [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) |
|
||||
| [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [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/) | [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/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) |
|
||||
| [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) |
|
||||
| [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex](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 -->
|
||||
|
||||
|
@ -78,6 +78,7 @@ func allDNSCodes() string {
|
||||
"namesilo",
|
||||
"netcup",
|
||||
"netlify",
|
||||
"nicmanager",
|
||||
"nifcloud",
|
||||
"njalla",
|
||||
"ns1",
|
||||
@ -1472,6 +1473,31 @@ func displayDNSHelp(name string) error {
|
||||
ew.writeln()
|
||||
ew.writeln(`More information: https://go-acme.github.io/lego/dns/netlify`)
|
||||
|
||||
case "nicmanager":
|
||||
// generated from: providers/dns/nicmanager/nicmanager.toml
|
||||
ew.writeln(`Configuration for Nicmanager.`)
|
||||
ew.writeln(`Code: 'nicmanager'`)
|
||||
ew.writeln(`Since: 'v4.5.0'`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Credentials:`)
|
||||
ew.writeln(` - "NICMANAGER_API_EMAIL": Email-based login`)
|
||||
ew.writeln(` - "NICMANAGER_API_LOGIN": Login, used for Username-based login`)
|
||||
ew.writeln(` - "NICMANAGER_API_PASSWORD": Password, always required`)
|
||||
ew.writeln(` - "NICMANAGER_API_USERNAME": Username, used for Username-based login`)
|
||||
ew.writeln()
|
||||
|
||||
ew.writeln(`Additional Configuration:`)
|
||||
ew.writeln(` - "NICMANAGER_API_MODE": mode: 'anycast' or 'zone' (default: 'anycast')`)
|
||||
ew.writeln(` - "NICMANAGER_API_OTP": TOTP Secret (optional)`)
|
||||
ew.writeln(` - "NICMANAGER_HTTP_TIMEOUT": API request timeout`)
|
||||
ew.writeln(` - "NICMANAGER_POLLING_INTERVAL": Time between DNS propagation check`)
|
||||
ew.writeln(` - "NICMANAGER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
|
||||
ew.writeln(` - "NICMANAGER_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/nicmanager`)
|
||||
|
||||
case "nifcloud":
|
||||
// generated from: providers/dns/nifcloud/nifcloud.toml
|
||||
ew.writeln(`Configuration for NIFCloud.`)
|
||||
|
89
docs/content/dns/zz_gen_nicmanager.md
Normal file
89
docs/content/dns/zz_gen_nicmanager.md
Normal file
@ -0,0 +1,89 @@
|
||||
---
|
||||
title: "Nicmanager"
|
||||
date: 2019-03-03T16:39:46+01:00
|
||||
draft: false
|
||||
slug: nicmanager
|
||||
---
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/nicmanager/nicmanager.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
|
||||
Since: v4.5.0
|
||||
|
||||
Configuration for [Nicmanager](https://www.nicmanager.com/).
|
||||
|
||||
|
||||
<!--more-->
|
||||
|
||||
- Code: `nicmanager`
|
||||
|
||||
Here is an example bash command using the Nicmanager provider:
|
||||
|
||||
```bash
|
||||
## Login using email
|
||||
|
||||
NICMANAGER_API_EMAIL = "foo@bar.baz" \
|
||||
NICMANAGER_API_PASSWORD = "password" \
|
||||
|
||||
# Optionally, if your account has TOTP enabled, set the secret here
|
||||
NICMANAGER_API_OTP = "long-secret" \
|
||||
|
||||
lego --email myemail@example.com --dns nicmanager --domains my.example.org run
|
||||
|
||||
## Login using account name + username
|
||||
|
||||
NICMANAGER_API_LOGIN = "myaccount" \
|
||||
NICMANAGER_API_USERNAME = "myuser" \
|
||||
NICMANAGER_API_PASSWORD = "password" \
|
||||
|
||||
# Optionally, if your account has TOTP enabled, set the secret here
|
||||
NICMANAGER_API_OTP = "long-secret" \
|
||||
|
||||
lego --email myemail@example.com --dns nicmanager --domains my.example.org run
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Credentials
|
||||
|
||||
| Environment Variable Name | Description |
|
||||
|-----------------------|-------------|
|
||||
| `NICMANAGER_API_EMAIL` | Email-based login |
|
||||
| `NICMANAGER_API_LOGIN` | Login, used for Username-based login |
|
||||
| `NICMANAGER_API_PASSWORD` | Password, always required |
|
||||
| `NICMANAGER_API_USERNAME` | Username, used for Username-based login |
|
||||
|
||||
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 |
|
||||
|--------------------------------|-------------|
|
||||
| `NICMANAGER_API_MODE` | mode: 'anycast' or 'zone' (default: 'anycast') |
|
||||
| `NICMANAGER_API_OTP` | TOTP Secret (optional) |
|
||||
| `NICMANAGER_HTTP_TIMEOUT` | API request timeout |
|
||||
| `NICMANAGER_POLLING_INTERVAL` | Time between DNS propagation check |
|
||||
| `NICMANAGER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
|
||||
| `NICMANAGER_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).
|
||||
|
||||
## Description
|
||||
|
||||
You can login using your account name + username or using your email address.
|
||||
Optionally if TOTP is configured for your account, set `NICMANAGER_API_OTP`.
|
||||
|
||||
|
||||
|
||||
## More information
|
||||
|
||||
- [API documentation](https://api.nicmanager.com/docs/v1/)
|
||||
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
||||
<!-- providers/dns/nicmanager/nicmanager.toml -->
|
||||
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
|
@ -69,6 +69,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
"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/nifcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/njalla"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ns1"
|
||||
@ -234,6 +235,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
|
||||
return netcup.NewDNSProvider()
|
||||
case "netlify":
|
||||
return netlify.NewDNSProvider()
|
||||
case "nicmanager":
|
||||
return nicmanager.NewDNSProvider()
|
||||
case "nifcloud":
|
||||
return nifcloud.NewDNSProvider()
|
||||
case "njalla":
|
||||
|
185
providers/dns/nicmanager/internal/client.go
Normal file
185
providers/dns/nicmanager/internal/client.go
Normal file
@ -0,0 +1,185 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://api.nicmanager.com/v1"
|
||||
headerTOTPToken = "X-Auth-Token"
|
||||
)
|
||||
|
||||
// Modes.
|
||||
const (
|
||||
ModeAnycast = "anycast"
|
||||
ModeZone = "zone"
|
||||
)
|
||||
|
||||
// Options the Client options.
|
||||
type Options struct {
|
||||
Login string
|
||||
Username string
|
||||
|
||||
Email string
|
||||
|
||||
Password string
|
||||
OTP string
|
||||
|
||||
Mode string
|
||||
}
|
||||
|
||||
// Client a nicmanager DNS client.
|
||||
type Client struct {
|
||||
HTTPClient *http.Client
|
||||
baseURL *url.URL
|
||||
|
||||
username string
|
||||
password string
|
||||
otp string
|
||||
|
||||
mode string
|
||||
}
|
||||
|
||||
// NewClient create a new Client.
|
||||
func NewClient(opts Options) *Client {
|
||||
c := &Client{
|
||||
mode: ModeAnycast,
|
||||
username: opts.Email,
|
||||
password: opts.Password,
|
||||
otp: opts.OTP,
|
||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
c.baseURL, _ = url.Parse(defaultBaseURL)
|
||||
|
||||
if opts.Mode != "" {
|
||||
c.mode = opts.Mode
|
||||
}
|
||||
|
||||
if opts.Login != "" && opts.Username != "" {
|
||||
c.username = fmt.Sprintf("%s.%s", opts.Login, opts.Username)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Client) GetZone(name string) (*Zone, error) {
|
||||
resp, err := c.do(http.MethodGet, name, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
msg := APIError{StatusCode: resp.StatusCode}
|
||||
if err = json.Unmarshal(b, &msg); err != nil {
|
||||
return nil, fmt.Errorf("failed to get zone info for %s", name)
|
||||
}
|
||||
|
||||
return nil, msg
|
||||
}
|
||||
|
||||
var zone Zone
|
||||
err = json.NewDecoder(resp.Body).Decode(&zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &zone, nil
|
||||
}
|
||||
|
||||
func (c Client) AddRecord(zone string, req RecordCreateUpdate) error {
|
||||
resp, err := c.do(http.MethodPost, path.Join(zone, "records"), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
msg := APIError{StatusCode: resp.StatusCode}
|
||||
if err = json.Unmarshal(b, &msg); err != nil {
|
||||
return fmt.Errorf("records create should've returned %d but returned %d", http.StatusAccepted, resp.StatusCode)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) DeleteRecord(zone string, record int) error {
|
||||
resp, err := c.do(http.MethodDelete, path.Join(zone, "records", strconv.Itoa(record)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
msg := APIError{StatusCode: resp.StatusCode}
|
||||
if err = json.Unmarshal(b, &msg); err != nil {
|
||||
return fmt.Errorf("records delete should've returned %d but returned %d", http.StatusAccepted, resp.StatusCode)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Client) do(method, uri string, body interface{}) (*http.Response, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonValue, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqBody = bytes.NewBuffer(jsonValue)
|
||||
}
|
||||
|
||||
endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, c.mode, uri))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := http.NewRequest(method, endpoint.String(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Header.Set("Accept", "application/json")
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
r.SetBasicAuth(c.username, c.password)
|
||||
|
||||
if c.otp != "" {
|
||||
tan, err := totp.GenerateCode(c.otp, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Header.Set(headerTOTPToken, tan)
|
||||
}
|
||||
|
||||
return c.HTTPClient.Do(r)
|
||||
}
|
145
providers/dns/nicmanager/internal/client_test.go
Normal file
145
providers/dns/nicmanager/internal/client_test.go
Normal file
@ -0,0 +1,145 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient_GetZone(t *testing.T) {
|
||||
client := setupTest(t, "/anycast/nicmanager-anycastdns4.net", testHandler(http.MethodGet, http.StatusOK, "zone.json"))
|
||||
|
||||
zone, err := client.GetZone("nicmanager-anycastdns4.net")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &Zone{
|
||||
Name: "nicmanager-anycastdns4.net",
|
||||
Active: true,
|
||||
Records: []Record{
|
||||
{
|
||||
ID: 186,
|
||||
Name: "nicmanager-anycastdns4.net",
|
||||
Type: "A",
|
||||
Content: "123.123.123.123",
|
||||
TTL: 3600,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, zone)
|
||||
}
|
||||
|
||||
func TestClient_GetZone_error(t *testing.T) {
|
||||
client := setupTest(t, "/anycast/foo", testHandler(http.MethodGet, http.StatusNotFound, "error.json"))
|
||||
|
||||
_, err := client.GetZone("foo")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestClient_AddRecord(t *testing.T) {
|
||||
client := setupTest(t, "/anycast/zonedomain.tld/records", testHandler(http.MethodPost, http.StatusAccepted, "error.json"))
|
||||
|
||||
record := RecordCreateUpdate{
|
||||
Type: "TXT",
|
||||
Name: "lego",
|
||||
Value: "content",
|
||||
TTL: 3600,
|
||||
}
|
||||
|
||||
err := client.AddRecord("zonedomain.tld", record)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_AddRecord_error(t *testing.T) {
|
||||
client := setupTest(t, "/anycast/zonedomain.tld", testHandler(http.MethodPost, http.StatusUnauthorized, "error.json"))
|
||||
|
||||
record := RecordCreateUpdate{
|
||||
Type: "TXT",
|
||||
Name: "zonedomain.tld",
|
||||
Value: "content",
|
||||
TTL: 3600,
|
||||
}
|
||||
|
||||
err := client.AddRecord("zonedomain.tld", record)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestClient_DeleteRecord(t *testing.T) {
|
||||
client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusAccepted, "error.json"))
|
||||
|
||||
err := client.DeleteRecord("zonedomain.tld", 6)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_DeleteRecord_error(t *testing.T) {
|
||||
client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusNoContent, ""))
|
||||
|
||||
err := client.DeleteRecord("zonedomain.tld", 7)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T, path string, handler http.Handler) *Client {
|
||||
t.Helper()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
mux.Handle(path, handler)
|
||||
|
||||
opts := Options{
|
||||
Login: "foo",
|
||||
Username: "bar",
|
||||
Password: "foo",
|
||||
OTP: "2hsn",
|
||||
}
|
||||
|
||||
client := NewClient(opts)
|
||||
client.HTTPClient = server.Client()
|
||||
client.baseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != method {
|
||||
http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok || username != "foo.bar" || password != "foo" {
|
||||
http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rw.WriteHeader(statusCode)
|
||||
|
||||
if statusCode == http.StatusNoContent {
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Open(filepath.Join("fixtures", filename))
|
||||
if err != nil {
|
||||
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
_, err = io.Copy(rw, file)
|
||||
if err != nil {
|
||||
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
3
providers/dns/nicmanager/internal/fixtures/error.json
Normal file
3
providers/dns/nicmanager/internal/fixtures/error.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"message": "Not Found"
|
||||
}
|
51
providers/dns/nicmanager/internal/fixtures/zone.json
Normal file
51
providers/dns/nicmanager/internal/fixtures/zone.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"order_id": 9053,
|
||||
"name": "nicmanager-anycastdns4.net",
|
||||
"order_status": "active",
|
||||
"event_status": "done",
|
||||
"active": true,
|
||||
"dnssec": "inactive",
|
||||
"master1": null,
|
||||
"master2": null,
|
||||
"soa": {
|
||||
"primary": "ns1.nic53.net",
|
||||
"mail": "hostmaster.nicmanager.de",
|
||||
"serial": 1481109046,
|
||||
"refresh": 14400,
|
||||
"retry": 1800,
|
||||
"expire": 1209600,
|
||||
"default": 3600,
|
||||
"ttl": 86400
|
||||
},
|
||||
"updated_datetime": "2016-09-02T13:52:18Z",
|
||||
"order_datetime": "2016-09-02T13:52:18Z",
|
||||
"records": [
|
||||
{
|
||||
"id": 186,
|
||||
"name": "nicmanager-anycastdns4.net",
|
||||
"type": "A",
|
||||
"content": "123.123.123.123",
|
||||
"ttl": 3600,
|
||||
"priority": 0,
|
||||
"active": true,
|
||||
"updated_datetime": "2016-09-02T13:52:18Z"
|
||||
}
|
||||
],
|
||||
"redirects": [
|
||||
{
|
||||
"id": 10,
|
||||
"name": "test.nicmanager-anycastdns4.net",
|
||||
"target": "https:\/\/www.nicmanager.com\/",
|
||||
"type": "frame",
|
||||
"updated_datetime": "2016-12-05T14:40:47Z",
|
||||
"request_uri": true,
|
||||
"ssl": false,
|
||||
"meta": {
|
||||
"title": "My frame",
|
||||
"keywords": "foo,bar",
|
||||
"description": "Just a Test"
|
||||
},
|
||||
"subdomain": "test"
|
||||
}
|
||||
]
|
||||
}
|
34
providers/dns/nicmanager/internal/types.go
Normal file
34
providers/dns/nicmanager/internal/types.go
Normal file
@ -0,0 +1,34 @@
|
||||
package internal
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Record struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Records []Record `json:"records"`
|
||||
}
|
||||
|
||||
type RecordCreateUpdate struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
TTL int `json:"ttl"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
StatusCode int `json:"-"`
|
||||
}
|
||||
|
||||
func (a APIError) Error() string {
|
||||
return fmt.Sprintf("%d: %s", a.StatusCode, a.Message)
|
||||
}
|
200
providers/dns/nicmanager/nicmanager.go
Normal file
200
providers/dns/nicmanager/nicmanager.go
Normal file
@ -0,0 +1,200 @@
|
||||
// Package nicmanager implements a DNS provider for solving the DNS-01 challenge using nicmanager DNS.
|
||||
package nicmanager
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/platform/config/env"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nicmanager/internal"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
envNamespace = "NICMANAGER_"
|
||||
|
||||
EnvLogin = envNamespace + "API_LOGIN"
|
||||
EnvUsername = envNamespace + "API_USERNAME"
|
||||
EnvEmail = envNamespace + "API_EMAIL"
|
||||
EnvPassword = envNamespace + "API_PASSWORD"
|
||||
EnvOTP = envNamespace + "API_OTP"
|
||||
EnvMode = envNamespace + "MODE"
|
||||
|
||||
EnvTTL = envNamespace + "TTL"
|
||||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||
)
|
||||
|
||||
const minTTL = 900
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider.
|
||||
type Config struct {
|
||||
Login string
|
||||
Username string
|
||||
Email string
|
||||
Password string
|
||||
OTPSecret string
|
||||
Mode 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, minTTL),
|
||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
|
||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
client *internal.Client
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for nicmanager.
|
||||
// Credentials must be passed in the environment variables:
|
||||
// NICMANAGER_API_LOGIN, NICMANAGER_API_USERNAME
|
||||
// NICMANAGER_API_EMAIL
|
||||
// NICMANAGER_API_PASSWORD
|
||||
// NICMANAGER_API_OTP
|
||||
// NICMANAGER_API_MODE.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get(EnvPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nicmanager: %w", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.Password = values[EnvPassword]
|
||||
|
||||
config.Mode = env.GetOrDefaultString(EnvMode, internal.ModeAnycast)
|
||||
config.Username = env.GetOrFile(EnvUsername)
|
||||
config.Login = env.GetOrFile(EnvLogin)
|
||||
config.Email = env.GetOrFile(EnvEmail)
|
||||
config.OTPSecret = env.GetOrFile(EnvOTP)
|
||||
|
||||
if config.TTL < minTTL {
|
||||
return nil, fmt.Errorf("TTL must be higher than %d: %d", minTTL, config.TTL)
|
||||
}
|
||||
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider instance configured for nicmanager.
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("nicmanager: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
opts := internal.Options{
|
||||
Password: config.Password,
|
||||
OTP: config.OTPSecret,
|
||||
Mode: config.Mode,
|
||||
}
|
||||
|
||||
switch {
|
||||
case config.Password == "":
|
||||
return nil, errors.New("nicmanager: credentials missing")
|
||||
case config.Email != "":
|
||||
opts.Email = config.Email
|
||||
case config.Login != "" && config.Username != "":
|
||||
opts.Login = config.Login
|
||||
opts.Username = config.Username
|
||||
default:
|
||||
return nil, errors.New("nicmanager: credentials missing")
|
||||
}
|
||||
|
||||
client := internal.NewClient(opts)
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
return &DNSProvider{client: client, config: config}, 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 to fulfill the dns-01 challenge.
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value := dns01.GetRecord(domain, keyAuth)
|
||||
|
||||
rootDomain, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("nicmanager: could not determine zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
zone, err := d.client.GetZone(dns01.UnFqdn(rootDomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err)
|
||||
}
|
||||
|
||||
// The way nic manager deals with record with multiple values is that they are completely different records with unique ids
|
||||
// Hence we don't check for an existing record here, but rather just create one
|
||||
record := internal.RecordCreateUpdate{
|
||||
Name: fqdn,
|
||||
Type: "TXT",
|
||||
TTL: d.config.TTL,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
err = d.client.AddRecord(zone.Name, record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nicmanager: failed to create record [zone: %q, fqdn: %q]: %w", zone.Name, fqdn, 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)
|
||||
|
||||
rootDomain, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("nicmanager: could not determine zone for domain %q: %w", domain, err)
|
||||
}
|
||||
|
||||
zone, err := d.client.GetZone(dns01.UnFqdn(rootDomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err)
|
||||
}
|
||||
|
||||
name := dns01.UnFqdn(fqdn)
|
||||
|
||||
var existingRecord internal.Record
|
||||
var existingRecordFound bool
|
||||
for _, record := range zone.Records {
|
||||
if strings.EqualFold(record.Type, "TXT") && strings.EqualFold(record.Name, name) && record.Content == value {
|
||||
existingRecord = record
|
||||
existingRecordFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if existingRecordFound {
|
||||
err = d.client.DeleteRecord(zone.Name, existingRecord.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nicmanager: failed to delete record [zone: %q, domain: %q]: %w", zone.Name, name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("nicmanager: no record found to cleanup")
|
||||
}
|
52
providers/dns/nicmanager/nicmanager.toml
Normal file
52
providers/dns/nicmanager/nicmanager.toml
Normal file
@ -0,0 +1,52 @@
|
||||
Name = "Nicmanager"
|
||||
Description = ''''''
|
||||
URL = "https://www.nicmanager.com/"
|
||||
Code = "nicmanager"
|
||||
Since = "v4.5.0"
|
||||
|
||||
Example = '''
|
||||
## Login using email
|
||||
|
||||
NICMANAGER_API_EMAIL = "foo@bar.baz" \
|
||||
NICMANAGER_API_PASSWORD = "password" \
|
||||
|
||||
# Optionally, if your account has TOTP enabled, set the secret here
|
||||
NICMANAGER_API_OTP = "long-secret" \
|
||||
|
||||
lego --email myemail@example.com --dns nicmanager --domains my.example.org run
|
||||
|
||||
## Login using account name + username
|
||||
|
||||
NICMANAGER_API_LOGIN = "myaccount" \
|
||||
NICMANAGER_API_USERNAME = "myuser" \
|
||||
NICMANAGER_API_PASSWORD = "password" \
|
||||
|
||||
# Optionally, if your account has TOTP enabled, set the secret here
|
||||
NICMANAGER_API_OTP = "long-secret" \
|
||||
|
||||
lego --email myemail@example.com --dns nicmanager --domains my.example.org run
|
||||
'''
|
||||
|
||||
Additional = '''
|
||||
## Description
|
||||
|
||||
You can login using your account name + username or using your email address.
|
||||
Optionally if TOTP is configured for your account, set `NICMANAGER_API_OTP`.
|
||||
'''
|
||||
|
||||
[Configuration]
|
||||
[Configuration.Credentials]
|
||||
NICMANAGER_API_LOGIN = "Login, used for Username-based login"
|
||||
NICMANAGER_API_USERNAME = "Username, used for Username-based login"
|
||||
NICMANAGER_API_EMAIL = "Email-based login"
|
||||
NICMANAGER_API_PASSWORD = "Password, always required"
|
||||
[Configuration.Additional]
|
||||
NICMANAGER_API_OTP = "TOTP Secret (optional)"
|
||||
NICMANAGER_API_MODE = "mode: 'anycast' or 'zone' (default: 'anycast')"
|
||||
NICMANAGER_POLLING_INTERVAL = "Time between DNS propagation check"
|
||||
NICMANAGER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
|
||||
NICMANAGER_TTL = "The TTL of the TXT record used for the DNS challenge"
|
||||
NICMANAGER_HTTP_TIMEOUT = "API request timeout"
|
||||
|
||||
[Links]
|
||||
API = "https://api.nicmanager.com/docs/v1/"
|
182
providers/dns/nicmanager/nicmanager_test.go
Normal file
182
providers/dns/nicmanager/nicmanager_test.go
Normal file
@ -0,0 +1,182 @@
|
||||
package nicmanager
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/platform/tester"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const envDomain = envNamespace + "DOMAIN"
|
||||
|
||||
var envTest = tester.NewEnvTest(EnvUsername, EnvLogin, EnvEmail, EnvPassword, EnvOTP).
|
||||
WithDomain(envDomain)
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success (email)",
|
||||
envVars: map[string]string{
|
||||
EnvEmail: "foo@example.com",
|
||||
EnvPassword: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "success (login.username)",
|
||||
envVars: map[string]string{
|
||||
EnvLogin: "foo",
|
||||
EnvUsername: "bar",
|
||||
EnvPassword: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "nicmanager: some credentials information are missing: NICMANAGER_API_PASSWORD",
|
||||
},
|
||||
{
|
||||
desc: "missing password",
|
||||
envVars: map[string]string{
|
||||
EnvEmail: "foo@example.com",
|
||||
},
|
||||
expected: "nicmanager: some credentials information are missing: NICMANAGER_API_PASSWORD",
|
||||
},
|
||||
{
|
||||
desc: "missing username",
|
||||
envVars: map[string]string{
|
||||
EnvLogin: "foo",
|
||||
EnvPassword: "secret",
|
||||
},
|
||||
expected: "nicmanager: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing login",
|
||||
envVars: map[string]string{
|
||||
EnvUsername: "bar",
|
||||
EnvPassword: "secret",
|
||||
},
|
||||
expected: "nicmanager: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
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 test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
login string
|
||||
username string
|
||||
email string
|
||||
password string
|
||||
otpSecret string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success (email)",
|
||||
email: "foo@example.com",
|
||||
password: "secret",
|
||||
},
|
||||
{
|
||||
desc: "success (login.username)",
|
||||
login: "john",
|
||||
username: "doe",
|
||||
password: "secret",
|
||||
},
|
||||
{
|
||||
desc: "missing credentials",
|
||||
expected: "nicmanager: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing password",
|
||||
email: "foo@example.com",
|
||||
expected: "nicmanager: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing login",
|
||||
login: "",
|
||||
username: "doe",
|
||||
password: "secret",
|
||||
expected: "nicmanager: credentials missing",
|
||||
},
|
||||
{
|
||||
desc: "missing username",
|
||||
login: "john",
|
||||
username: "",
|
||||
password: "secret",
|
||||
expected: "nicmanager: credentials missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.Login = test.login
|
||||
config.Username = test.username
|
||||
config.Email = test.email
|
||||
config.Password = test.password
|
||||
config.OTPSecret = test.otpSecret
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if test.expected == "" {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
require.NotNil(t, p.client)
|
||||
} 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user