joker: add support for SVC API (#1267)

This commit is contained in:
Ludovic Fernandez 2020-10-08 16:52:50 +02:00 committed by GitHub
parent abd783a124
commit fbab0e3c64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 919 additions and 407 deletions

View file

@ -975,7 +975,8 @@ func displayDNSHelp(name string) error {
ew.writeln() ew.writeln()
ew.writeln(`Credentials:`) ew.writeln(`Credentials:`)
ew.writeln(` - "JOKER_API_KEY": API key`) ew.writeln(` - "JOKER_API_KEY": API key (only with DMAPI mode)`)
ew.writeln(` - "JOKER_API_MODE": 'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI)`)
ew.writeln(` - "JOKER_PASSWORD": Joker.com password`) ew.writeln(` - "JOKER_PASSWORD": Joker.com password`)
ew.writeln(` - "JOKER_USERNAME": Joker.com username (email address)`) ew.writeln(` - "JOKER_USERNAME": Joker.com username (email address)`)
ew.writeln() ew.writeln()

View file

@ -21,10 +21,19 @@ Configuration for [Joker](https://joker.com).
Here is an example bash command using the Joker provider: Here is an example bash command using the Joker provider:
```bash ```bash
# SVC
JOKER_API_MODE=SVC \
JOKER_USERNAME=<your email> \ JOKER_USERNAME=<your email> \
JOKER_PASSWORD=<your password> \ JOKER_PASSWORD=<your password> \
lego --dns joker --domains my.domain.com --email my@email.com run lego --dns joker --domains my.domain.com --email my@email.com run
# or
# DMAPI
JOKER_API_MODE=DMAPI \
JOKER_USERNAME=<your email> \
JOKER_PASSWORD=<your password> \
lego --dns joker --domains my.domain.com --email my@email.com run
## or
JOKER_API_MODE=DMAPI \
JOKER_API_KEY=<your API key> \ JOKER_API_KEY=<your API key> \
lego --dns joker --domains my.domain.com --email my@email.com run lego --dns joker --domains my.domain.com --email my@email.com run
``` ```
@ -36,7 +45,8 @@ lego --dns joker --domains my.domain.com --email my@email.com run
| Environment Variable Name | Description | | Environment Variable Name | Description |
|-----------------------|-------------| |-----------------------|-------------|
| `JOKER_API_KEY` | API key | | `JOKER_API_KEY` | API key (only with DMAPI mode) |
| `JOKER_API_MODE` | 'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI) |
| `JOKER_PASSWORD` | Joker.com password | | `JOKER_PASSWORD` | Joker.com password |
| `JOKER_USERNAME` | Joker.com username (email address) | | `JOKER_USERNAME` | Joker.com username (email address) |

View file

@ -1,4 +1,6 @@
package joker // Package dmapi Client for DMAPI joker.com.
// https://joker.com/faq/category/39/22-dmapi.html
package dmapi
import ( import (
"errors" "errors"
@ -6,6 +8,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"path"
"strconv" "strconv"
"strings" "strings"
@ -15,8 +18,8 @@ import (
const defaultBaseURL = "https://dmapi.joker.com/request/" const defaultBaseURL = "https://dmapi.joker.com/request/"
// Joker DMAPI Response. // Response Joker DMAPI Response.
type response struct { type Response struct {
Headers url.Values Headers url.Values
Body string Body string
StatusCode int StatusCode int
@ -24,9 +27,143 @@ type response struct {
AuthSid string AuthSid string
} }
type AuthInfo struct {
APIKey string
Username string
Password string
authSid string
}
// Client a DMAPI Client.
type Client struct {
HTTPClient *http.Client
BaseURL string
Debug bool
auth AuthInfo
}
// NewClient creates a new DMAPI Client.
func NewClient(auth AuthInfo) *Client {
return &Client{
HTTPClient: http.DefaultClient,
BaseURL: defaultBaseURL,
Debug: false,
auth: auth,
}
}
// Login performs a login to Joker's DMAPI.
func (c *Client) Login() (*Response, error) {
if c.auth.authSid != "" {
// already logged in
return nil, nil
}
var values url.Values
switch {
case c.auth.Username != "" && c.auth.Password != "":
values = url.Values{
"username": {c.auth.Username},
"password": {c.auth.Password},
}
case c.auth.APIKey != "":
values = url.Values{"api-key": {c.auth.APIKey}}
default:
return nil, errors.New("no username and password or api-key")
}
response, err := c.postRequest("login", values)
if err != nil {
return response, err
}
if response == nil {
return nil, errors.New("login returned nil response")
}
if response.AuthSid == "" {
return response, errors.New("login did not return valid Auth-Sid")
}
c.auth.authSid = response.AuthSid
return response, nil
}
// Logout closes authenticated session with Joker's DMAPI.
func (c *Client) Logout() (*Response, error) {
if c.auth.authSid == "" {
return nil, errors.New("already logged out")
}
response, err := c.postRequest("logout", url.Values{})
if err == nil {
c.auth.authSid = ""
}
return response, err
}
// GetZone returns content of DNS zone for domain.
func (c *Client) GetZone(domain string) (*Response, error) {
if c.auth.authSid == "" {
return nil, errors.New("must be logged in to get zone")
}
return c.postRequest("dns-zone-get", url.Values{"domain": {dns01.UnFqdn(domain)}})
}
// PutZone uploads DNS zone to Joker DMAPI.
func (c *Client) PutZone(domain, zone string) (*Response, error) {
if c.auth.authSid == "" {
return nil, errors.New("must be logged in to put zone")
}
return c.postRequest("dns-zone-put", url.Values{"domain": {dns01.UnFqdn(domain)}, "zone": {strings.TrimSpace(zone)}})
}
// postRequest performs actual HTTP request.
func (c *Client) postRequest(cmd string, data url.Values) (*Response, error) {
baseURL, err := url.Parse(c.BaseURL)
if err != nil {
return nil, err
}
endpoint, err := baseURL.Parse(path.Join(baseURL.Path, cmd))
if err != nil {
return nil, err
}
if c.auth.authSid != "" {
data.Set("auth-sid", c.auth.authSid)
}
if c.Debug {
log.Infof("postRequest:\n\tURL: %q\n\tData: %v", endpoint.String(), data)
}
resp, err := c.HTTPClient.PostForm(endpoint.String(), data)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP error %d [%s]: %v", resp.StatusCode, http.StatusText(resp.StatusCode), string(body))
}
return parseResponse(string(body)), nil
}
// parseResponse parses HTTP response body. // parseResponse parses HTTP response body.
func parseResponse(message string) *response { func parseResponse(message string) *Response {
r := &response{Headers: url.Values{}, StatusCode: -1} r := &Response{Headers: url.Values{}, StatusCode: -1}
parts := strings.SplitN(message, "\n\n", 2) parts := strings.SplitN(message, "\n\n", 2)
@ -64,105 +201,6 @@ func parseResponse(message string) *response {
return r return r
} }
// login performs a login to Joker's DMAPI.
func (d *DNSProvider) login() (*response, error) {
if d.config.AuthSid != "" {
// already logged in
return nil, nil
}
var values url.Values
switch {
case d.config.Username != "" && d.config.Password != "":
values = url.Values{
"username": {d.config.Username},
"password": {d.config.Password},
}
case d.config.APIKey != "":
values = url.Values{"api-key": {d.config.APIKey}}
default:
return nil, errors.New("no username and password or api-key")
}
response, err := d.postRequest("login", values)
if err != nil {
return response, err
}
if response == nil {
return nil, errors.New("login returned nil response")
}
if response.AuthSid == "" {
return response, errors.New("login did not return valid Auth-Sid")
}
d.config.AuthSid = response.AuthSid
return response, nil
}
// logout closes authenticated session with Joker's DMAPI.
func (d *DNSProvider) logout() (*response, error) {
if d.config.AuthSid == "" {
return nil, errors.New("already logged out")
}
response, err := d.postRequest("logout", url.Values{})
if err == nil {
d.config.AuthSid = ""
}
return response, err
}
// getZone returns content of DNS zone for domain.
func (d *DNSProvider) getZone(domain string) (*response, error) {
if d.config.AuthSid == "" {
return nil, errors.New("must be logged in to get zone")
}
return d.postRequest("dns-zone-get", url.Values{"domain": {dns01.UnFqdn(domain)}})
}
// putZone uploads DNS zone to Joker DMAPI.
func (d *DNSProvider) putZone(domain, zone string) (*response, error) {
if d.config.AuthSid == "" {
return nil, errors.New("must be logged in to put zone")
}
return d.postRequest("dns-zone-put", url.Values{"domain": {dns01.UnFqdn(domain)}, "zone": {strings.TrimSpace(zone)}})
}
// postRequest performs actual HTTP request.
func (d *DNSProvider) postRequest(cmd string, data url.Values) (*response, error) {
uri := d.config.BaseURL + cmd
if d.config.AuthSid != "" {
data.Set("auth-sid", d.config.AuthSid)
}
if d.config.Debug {
log.Infof("postRequest:\n\tURL: %q\n\tData: %v", uri, data)
}
resp, err := d.config.HTTPClient.PostForm(uri, data)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP error %d [%s]: %v", resp.StatusCode, http.StatusText(resp.StatusCode), string(body))
}
return parseResponse(string(body)), nil
}
// Temporary workaround, until it get fixed on API side. // Temporary workaround, until it get fixed on API side.
func fixTxtLines(line string) string { func fixTxtLines(line string) string {
fields := strings.Fields(line) fields := strings.Fields(line)
@ -179,8 +217,8 @@ func fixTxtLines(line string) string {
return strings.Join(fields, " ") return strings.Join(fields, " ")
} }
// removeTxtEntryFromZone clean-ups all TXT records with given name. // RemoveTxtEntryFromZone clean-ups all TXT records with given name.
func removeTxtEntryFromZone(zone, relative string) (string, bool) { func RemoveTxtEntryFromZone(zone, relative string) (string, bool) {
prefix := fmt.Sprintf("%s TXT 0 ", relative) prefix := fmt.Sprintf("%s TXT 0 ", relative)
modified := false modified := false
@ -196,8 +234,8 @@ func removeTxtEntryFromZone(zone, relative string) (string, bool) {
return strings.TrimSpace(strings.Join(zoneEntries, "\n")), modified return strings.TrimSpace(strings.Join(zoneEntries, "\n")), modified
} }
// addTxtEntryToZone returns DNS zone with added TXT record. // AddTxtEntryToZone returns DNS zone with added TXT record.
func addTxtEntryToZone(zone, relative, value string, ttl int) string { func AddTxtEntryToZone(zone, relative, value string, ttl int) string {
var zoneEntries []string var zoneEntries []string
for _, line := range strings.Split(zone, "\n") { for _, line := range strings.Split(zone, "\n") {

View file

@ -1,4 +1,4 @@
package joker package dmapi
import ( import (
"io" "io"
@ -23,10 +23,13 @@ const (
serverErrorUsername = "error" serverErrorUsername = "error"
) )
func setup() (*http.ServeMux, *httptest.Server) { func setup(t *testing.T) (*http.ServeMux, string) {
mux := http.NewServeMux() mux := http.NewServeMux()
server := httptest.NewServer(mux) server := httptest.NewServer(mux)
return mux, server t.Cleanup(server.Close)
return mux, server.URL
} }
func TestDNSProvider_login_api_key(t *testing.T) { func TestDNSProvider_login_api_key(t *testing.T) {
@ -63,11 +66,10 @@ func TestDNSProvider_login_api_key(t *testing.T) {
}, },
} }
mux, server := setup() mux, serverURL := setup(t)
defer server.Close()
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method) require.Equal(t, http.MethodPost, r.Method)
switch r.FormValue("api-key") { switch r.FormValue("api-key") {
case correctAPIKey: case correctAPIKey:
@ -83,15 +85,10 @@ func TestDNSProvider_login_api_key(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig() client := NewClient(AuthInfo{APIKey: test.apiKey})
config.BaseURL = server.URL client.BaseURL = serverURL
config.APIKey = test.apiKey
p, err := NewDNSProviderConfig(config) response, err := client.Login()
require.NoError(t, err)
require.NotNil(t, p)
response, err := p.login()
if test.expectedError { if test.expectedError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -144,11 +141,10 @@ func TestDNSProvider_login_username(t *testing.T) {
}, },
} }
mux, server := setup() mux, serverURL := setup(t)
defer server.Close()
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method) require.Equal(t, http.MethodPost, r.Method)
switch r.FormValue("username") { switch r.FormValue("username") {
case correctUsername: case correctUsername:
@ -164,16 +160,10 @@ func TestDNSProvider_login_username(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig() client := NewClient(AuthInfo{Username: test.username, Password: test.password})
config.BaseURL = server.URL client.BaseURL = serverURL
config.Username = test.username
config.Password = test.password
p, err := NewDNSProviderConfig(config) response, err := client.Login()
require.NoError(t, err)
require.NotNil(t, p)
response, err := p.login()
if test.expectedError { if test.expectedError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -215,11 +205,10 @@ func TestDNSProvider_logout(t *testing.T) {
}, },
} }
mux, server := setup() mux, serverURL := setup(t)
defer server.Close()
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method) require.Equal(t, http.MethodPost, r.Method)
switch r.FormValue("auth-sid") { switch r.FormValue("auth-sid") {
case correctAPIKey: case correctAPIKey:
@ -233,16 +222,10 @@ func TestDNSProvider_logout(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig() client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid})
config.BaseURL = server.URL client.BaseURL = serverURL
config.APIKey = "12345"
config.AuthSid = test.authSid
p, err := NewDNSProviderConfig(config) response, err := client.Logout()
require.NoError(t, err)
require.NotNil(t, p)
response, err := p.logout()
if test.expectedError { if test.expectedError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -291,11 +274,10 @@ func TestDNSProvider_getZone(t *testing.T) {
}, },
} }
mux, server := setup() mux, serverURL := setup(t)
defer server.Close()
mux.HandleFunc("/dns-zone-get", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/dns-zone-get", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST", r.Method) require.Equal(t, http.MethodPost, r.Method)
authSid := r.FormValue("auth-sid") authSid := r.FormValue("auth-sid")
domain := r.FormValue("domain") domain := r.FormValue("domain")
@ -312,16 +294,10 @@ func TestDNSProvider_getZone(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig() client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid})
config.BaseURL = server.URL client.BaseURL = serverURL
config.APIKey = "12345"
config.AuthSid = test.authSid
p, err := NewDNSProviderConfig(config) response, err := client.GetZone(test.domain)
require.NoError(t, err)
require.NotNil(t, p)
response, err := p.getZone(test.domain)
if test.expectedError { if test.expectedError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -338,12 +314,12 @@ func Test_parseResponse(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
input string input string
expected *response expected *Response
}{ }{
{ {
desc: "Empty response", desc: "Empty response",
input: "", input: "",
expected: &response{ expected: &Response{
Headers: url.Values{}, Headers: url.Values{},
StatusCode: -1, StatusCode: -1,
}, },
@ -351,7 +327,7 @@ func Test_parseResponse(t *testing.T) {
{ {
desc: "No headers, just body", desc: "No headers, just body",
input: "\n\nTest body", input: "\n\nTest body",
expected: &response{ expected: &Response{
Headers: url.Values{}, Headers: url.Values{},
Body: "Test body", Body: "Test body",
StatusCode: -1, StatusCode: -1,
@ -360,7 +336,7 @@ func Test_parseResponse(t *testing.T) {
{ {
desc: "Headers and body", desc: "Headers and body",
input: "Test-Header: value\n\nTest body", input: "Test-Header: value\n\nTest body",
expected: &response{ expected: &Response{
Headers: url.Values{"Test-Header": {"value"}}, Headers: url.Values{"Test-Header": {"value"}},
Body: "Test body", Body: "Test body",
StatusCode: -1, StatusCode: -1,
@ -369,7 +345,7 @@ func Test_parseResponse(t *testing.T) {
{ {
desc: "Headers and body + Auth-Sid", desc: "Headers and body + Auth-Sid",
input: "Test-Header: value\nAuth-Sid: 123\n\nTest body", input: "Test-Header: value\nAuth-Sid: 123\n\nTest body",
expected: &response{ expected: &Response{
Headers: url.Values{"Test-Header": {"value"}, "Auth-Sid": {"123"}}, Headers: url.Values{"Test-Header": {"value"}, "Auth-Sid": {"123"}},
Body: "Test body", Body: "Test body",
StatusCode: -1, StatusCode: -1,
@ -379,7 +355,7 @@ func Test_parseResponse(t *testing.T) {
{ {
desc: "Headers and body + Status-Text", desc: "Headers and body + Status-Text",
input: "Test-Header: value\nStatus-Text: OK\n\nTest body", input: "Test-Header: value\nStatus-Text: OK\n\nTest body",
expected: &response{ expected: &Response{
Headers: url.Values{"Test-Header": {"value"}, "Status-Text": {"OK"}}, Headers: url.Values{"Test-Header": {"value"}, "Status-Text": {"OK"}},
Body: "Test body", Body: "Test body",
StatusText: "OK", StatusText: "OK",
@ -389,7 +365,7 @@ func Test_parseResponse(t *testing.T) {
{ {
desc: "Headers and body + Status-Code", desc: "Headers and body + Status-Code",
input: "Test-Header: value\nStatus-Code: 2020\n\nTest body", input: "Test-Header: value\nStatus-Code: 2020\n\nTest body",
expected: &response{ expected: &Response{
Headers: url.Values{"Test-Header": {"value"}, "Status-Code": {"2020"}}, Headers: url.Values{"Test-Header": {"value"}, "Status-Code": {"2020"}},
Body: "Test body", Body: "Test body",
StatusCode: 2020, StatusCode: 2020,
@ -453,7 +429,7 @@ func Test_removeTxtEntryFromZone(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
zone, modified := removeTxtEntryFromZone(test.input, "_acme-challenge") zone, modified := RemoveTxtEntryFromZone(test.input, "_acme-challenge")
assert.Equal(t, zone, test.expected) assert.Equal(t, zone, test.expected)
assert.Equal(t, modified, test.modified) assert.Equal(t, modified, test.modified)
}) })
@ -485,7 +461,7 @@ func Test_addTxtEntryToZone(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
zone := addTxtEntryToZone(test.input, "_acme-challenge", "test", 120) zone := AddTxtEntryToZone(test.input, "_acme-challenge", "test", 120)
assert.Equal(t, zone, test.expected) assert.Equal(t, zone, test.expected)
}) })
} }

View file

@ -0,0 +1,72 @@
// Package svc Client for the SVC API.
// https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html
package svc
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
querystring "github.com/google/go-querystring/query"
)
const defaultBaseURL = "https://svc.joker.com/nic/replace"
type request struct {
Username string `url:"username"`
Password string `url:"password"`
Zone string `url:"zone"`
Label string `url:"label"`
Type string `url:"type"`
Value string `url:"value"`
}
type Client struct {
HTTPClient *http.Client
BaseURL string
username string
password string
}
func NewClient(username, password string) *Client {
return &Client{
HTTPClient: http.DefaultClient,
BaseURL: defaultBaseURL,
username: username,
password: password,
}
}
func (c *Client) Send(zone, label, value string) error {
req := request{
Username: c.username,
Password: c.password,
Zone: zone,
Label: label,
Type: "TXT",
Value: value,
}
v, err := querystring.Values(req)
if err != nil {
return err
}
resp, err := c.HTTPClient.PostForm(c.BaseURL, v)
if err != nil {
return err
}
all, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode == http.StatusOK && strings.HasPrefix(string(all), "OK") {
return nil
}
return fmt.Errorf("error: %d: %s", resp.StatusCode, string(all))
}

View file

@ -0,0 +1,79 @@
package svc
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
)
func TestClient_Send(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
return
}
all, _ := ioutil.ReadAll(req.Body)
if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=123&zone=example.com" {
http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest)
return
}
_, err := rw.Write([]byte("OK: 1 inserted, 0 deleted"))
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
client := NewClient("test", "secret")
client.BaseURL = server.URL
zone := "example.com"
label := "_acme-challenge"
value := "123"
err := client.Send(zone, label, value)
require.NoError(t, err)
}
func TestClient_Send_empty(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
return
}
all, _ := ioutil.ReadAll(req.Body)
if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=&zone=example.com" {
http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest)
return
}
_, err := rw.Write([]byte("OK: 1 inserted, 0 deleted"))
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
client := NewClient("test", "secret")
client.BaseURL = server.URL
zone := "example.com"
label := "_acme-challenge"
value := ""
err := client.Send(zone, label, value)
require.NoError(t, err)
}

View file

@ -1,15 +1,13 @@
// Package joker implements a DNS provider for solving the DNS-01 challenge using joker.com DMAPI. // Package joker implements a DNS provider for solving the DNS-01 challenge using joker.com.
package joker package joker
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"strings" "os"
"time" "time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/config/env"
) )
@ -21,178 +19,64 @@ const (
EnvUsername = envNamespace + "USERNAME" EnvUsername = envNamespace + "USERNAME"
EnvPassword = envNamespace + "PASSWORD" EnvPassword = envNamespace + "PASSWORD"
EnvDebug = envNamespace + "DEBUG" EnvDebug = envNamespace + "DEBUG"
EnvMode = envNamespace + "API_MODE"
EnvTTL = envNamespace + "TTL" EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
) )
const (
modeDMAPI = "DMAPI"
modeSVC = "SVC"
)
// Config is used to configure the creation of the DNSProvider. // Config is used to configure the creation of the DNSProvider.
type Config struct { type Config struct {
Debug bool Debug bool
BaseURL string
APIKey string APIKey string
Username string Username string
Password string Password string
APIMode string
PropagationTimeout time.Duration PropagationTimeout time.Duration
PollingInterval time.Duration PollingInterval time.Duration
SequenceInterval time.Duration
TTL int TTL int
HTTPClient *http.Client HTTPClient *http.Client
AuthSid string
} }
// NewDefaultConfig returns a default configuration for the DNSProvider. // NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config { func NewDefaultConfig() *Config {
return &Config{ return &Config{
BaseURL: defaultBaseURL, APIMode: env.GetOrDefaultString(EnvMode, modeDMAPI),
Debug: env.GetOrDefaultBool(EnvDebug, false), Debug: env.GetOrDefaultBool(EnvDebug, false),
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second),
}, },
} }
} }
// DNSProvider implements the challenge.Provider interface. // NewDNSProvider returns a DNSProvider instance configured for Joker.
type DNSProvider struct {
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for Joker DMAPI.
// Credentials must be passed in the environment variable JOKER_API_KEY. // Credentials must be passed in the environment variable JOKER_API_KEY.
func NewDNSProvider() (*DNSProvider, error) { func NewDNSProvider() (challenge.ProviderTimeout, error) {
values, err := env.Get(EnvAPIKey) if os.Getenv(EnvMode) == modeSVC {
if err != nil { return newSvcProvider()
var errU error
values, errU = env.Get(EnvUsername, EnvPassword)
if errU != nil {
return nil, fmt.Errorf("joker: %v or %v", errU, err)
}
} }
config := NewDefaultConfig() return newDmapiProvider()
config.APIKey = values[EnvAPIKey]
config.Username = values[EnvUsername]
config.Password = values[EnvPassword]
return NewDNSProviderConfig(config)
} }
// NewDNSProviderConfig return a DNSProvider instance configured for Joker DMAPI. // NewDNSProviderConfig return a DNSProvider instance configured for Joker.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { func NewDNSProviderConfig(config *Config) (challenge.ProviderTimeout, error) {
if config == nil { if config.APIMode == modeSVC {
return nil, errors.New("joker: the configuration of the DNS provider is nil") return newSvcProviderConfig(config)
} }
if config.APIKey == "" { return newDmapiProviderConfig(config)
if config.Username == "" || config.Password == "" {
return nil, errors.New("joker: credentials missing")
}
}
if !strings.HasSuffix(config.BaseURL, "/") {
config.BaseURL += "/"
}
return &DNSProvider{config: config}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present installs a TXT record for the DNS challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %w", err)
}
relative := getRelative(fqdn, zone)
if d.config.Debug {
log.Infof("[%s] joker: adding TXT record %q to zone %q with value %q", domain, relative, zone, value)
}
response, err := d.login()
if err != nil {
return formatResponseError(response, err)
}
response, err = d.getZone(zone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
dnsZone := addTxtEntryToZone(response.Body, relative, value, d.config.TTL)
response, err = d.putZone(zone, dnsZone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
return nil
}
// CleanUp removes a TXT record used for a previous DNS challenge.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %w", err)
}
relative := getRelative(fqdn, zone)
if d.config.Debug {
log.Infof("[%s] joker: removing entry %q from zone %q", domain, relative, zone)
}
response, err := d.login()
if err != nil {
return formatResponseError(response, err)
}
defer func() {
// Try to logout in case of errors
_, _ = d.logout()
}()
response, err = d.getZone(zone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
dnsZone, modified := removeTxtEntryFromZone(response.Body, relative)
if modified {
response, err = d.putZone(zone, dnsZone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
}
response, err = d.logout()
if err != nil {
return formatResponseError(response, err)
}
return nil
}
func getRelative(fqdn, zone string) string {
return dns01.UnFqdn(strings.TrimSuffix(fqdn, dns01.ToFqdn(zone)))
}
// formatResponseError formats error with optional details from DMAPI response.
func formatResponseError(response *response, err error) error {
if response != nil {
return fmt.Errorf("joker: DMAPI error: %w Response: %v", err, response.Headers)
}
return fmt.Errorf("joker: DMAPI error: %w", err)
} }

View file

@ -5,19 +5,29 @@ Code = "joker"
Since = "v2.6.0" Since = "v2.6.0"
Example = ''' Example = '''
# SVC
JOKER_API_MODE=SVC \
JOKER_USERNAME=<your email> \ JOKER_USERNAME=<your email> \
JOKER_PASSWORD=<your password> \ JOKER_PASSWORD=<your password> \
lego --dns joker --domains my.domain.com --email my@email.com run lego --dns joker --domains my.domain.com --email my@email.com run
# or
# DMAPI
JOKER_API_MODE=DMAPI \
JOKER_USERNAME=<your email> \
JOKER_PASSWORD=<your password> \
lego --dns joker --domains my.domain.com --email my@email.com run
## or
JOKER_API_MODE=DMAPI \
JOKER_API_KEY=<your API key> \ JOKER_API_KEY=<your API key> \
lego --dns joker --domains my.domain.com --email my@email.com run lego --dns joker --domains my.domain.com --email my@email.com run
''' '''
[Configuration] [Configuration]
[Configuration.Credentials] [Configuration.Credentials]
JOKER_API_KEY = "API key" JOKER_API_MODE = "'DMAPI' or 'SVC'. DMAPI is for resellers accounts. (Default: DMAPI)"
JOKER_USERNAME = "Joker.com username (email address)" JOKER_USERNAME = "Joker.com username (email address)"
JOKER_PASSWORD = "Joker.com password" JOKER_PASSWORD = "Joker.com password"
JOKER_API_KEY = "API key (only with DMAPI mode)"
[Configuration.Additional] [Configuration.Additional]
JOKER_POLLING_INTERVAL = "Time between DNS propagation check" JOKER_POLLING_INTERVAL = "Time between DNS propagation check"
JOKER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" JOKER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
@ -26,3 +36,4 @@ lego --dns joker --domains my.domain.com --email my@email.com run
[Links] [Links]
API = "https://joker.com/faq/category/39/22-dmapi.html" API = "https://joker.com/faq/category/39/22-dmapi.html"
API_SVC = "https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html"

View file

@ -1,6 +1,8 @@
package joker package joker
import ( import (
"fmt"
"os"
"testing" "testing"
"time" "time"
@ -11,54 +13,40 @@ import (
const envDomain = envNamespace + "DOMAIN" const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(EnvAPIKey, EnvUsername, EnvPassword). var envTest = tester.NewEnvTest(EnvAPIKey, EnvUsername, EnvPassword, EnvMode).
WithDomain(envDomain) WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) { func TestNewDNSProvider(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
envVars map[string]string envVars map[string]string
expected string expected interface{}
}{ }{
{ {
desc: "success API key", desc: "mode DMAPI (default)",
envVars: map[string]string{
EnvAPIKey: "123",
},
},
{
desc: "success username password",
envVars: map[string]string{ envVars: map[string]string{
EnvUsername: "123", EnvUsername: "123",
EnvPassword: "123", EnvPassword: "123",
}, },
expected: &dmapiProvider{},
}, },
{ {
desc: "missing credentials", desc: "mode DMAPI",
envVars: map[string]string{ envVars: map[string]string{
EnvAPIKey: "", EnvMode: modeDMAPI,
EnvUsername: "",
EnvPassword: "",
},
expected: "joker: some credentials information are missing: JOKER_USERNAME,JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY",
},
{
desc: "missing password",
envVars: map[string]string{
EnvAPIKey: "",
EnvUsername: "123", EnvUsername: "123",
EnvPassword: "",
},
expected: "joker: some credentials information are missing: JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY",
},
{
desc: "missing username",
envVars: map[string]string{
EnvAPIKey: "",
EnvUsername: "",
EnvPassword: "123", EnvPassword: "123",
}, },
expected: "joker: some credentials information are missing: JOKER_USERNAME or some credentials information are missing: JOKER_API_KEY", expected: &dmapiProvider{},
},
{
desc: "mode SVC",
envVars: map[string]string{
EnvMode: modeSVC,
EnvUsername: "123",
EnvPassword: "123",
},
expected: &svcProvider{},
}, },
} }
@ -69,92 +57,51 @@ func TestNewDNSProvider(t *testing.T) {
envTest.Apply(test.envVars) envTest.Apply(test.envVars)
p, err := NewDNSProvider() fmt.Println(os.Getenv(EnvMode))
if len(test.expected) == 0 { p, err := NewDNSProvider()
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, p) require.NotNil(t, p)
assert.NotNil(t, p.config)
} else { assert.IsType(t, test.expected, p)
require.EqualError(t, err, test.expected)
}
}) })
} }
} }
func TestNewDNSProviderConfig(t *testing.T) { func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
apiKey string mode string
username string expected interface{}
password string
baseURL string
expected string
expectedBaseURL string
}{ }{
{ {
desc: "success api key", desc: "mode DMAPI (default)",
apiKey: "123", expected: &dmapiProvider{},
expectedBaseURL: defaultBaseURL,
}, },
{ {
desc: "success username and password", desc: "mode DMAPI",
username: "123", mode: modeDMAPI,
password: "123", expected: &dmapiProvider{},
expectedBaseURL: defaultBaseURL,
}, },
{ {
desc: "missing credentials", desc: "mode SVC",
expected: "joker: credentials missing", mode: modeSVC,
expectedBaseURL: defaultBaseURL, expected: &svcProvider{},
},
{
desc: "missing credentials: username",
expected: "joker: credentials missing",
username: "123",
expectedBaseURL: defaultBaseURL,
},
{
desc: "missing credentials: password",
expected: "joker: credentials missing",
password: "123",
expectedBaseURL: defaultBaseURL,
},
{
desc: "Base URL should ends with /",
apiKey: "123",
baseURL: "http://example.com",
expectedBaseURL: "http://example.com/",
},
{
desc: "Base URL already ends with /",
apiKey: "123",
baseURL: "http://example.com/",
expectedBaseURL: "http://example.com/",
}, },
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig() config := NewDefaultConfig()
config.APIKey = test.apiKey config.Username = "123"
config.Username = test.username config.Password = "123"
config.Password = test.password config.APIMode = test.mode
if test.baseURL != "" {
config.BaseURL = test.baseURL
}
p, err := NewDNSProviderConfig(config) p, err := NewDNSProviderConfig(config)
require.NoError(t, err)
require.NotNil(t, p)
if len(test.expected) == 0 { assert.IsType(t, test.expected, p)
require.NoError(t, err)
require.NotNil(t, p)
assert.NotNil(t, p.config)
assert.Equal(t, test.expectedBaseURL, p.config.BaseURL)
} else {
require.EqualError(t, err, test.expected)
}
}) })
} }
} }

View file

@ -0,0 +1,164 @@
package joker
import (
"errors"
"fmt"
"strings"
"time"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/go-acme/lego/v4/providers/dns/joker/internal/dmapi"
)
// dmapiProvider implements the challenge.Provider interface.
type dmapiProvider struct {
config *Config
client *dmapi.Client
}
// newDmapiProvider returns a DNSProvider instance configured for Joker.
// Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD or JOKER_API_KEY.
func newDmapiProvider() (*dmapiProvider, error) {
values, err := env.Get(EnvAPIKey)
if err != nil {
var errU error
values, errU = env.Get(EnvUsername, EnvPassword)
if errU != nil {
return nil, fmt.Errorf("joker: %v or %v", errU, err)
}
}
config := NewDefaultConfig()
config.APIKey = values[EnvAPIKey]
config.Username = values[EnvUsername]
config.Password = values[EnvPassword]
return newDmapiProviderConfig(config)
}
// newDmapiProviderConfig return a DNSProvider instance configured for Joker.
func newDmapiProviderConfig(config *Config) (*dmapiProvider, error) {
if config == nil {
return nil, errors.New("joker: the configuration of the DNS provider is nil")
}
if config.APIKey == "" {
if config.Username == "" || config.Password == "" {
return nil, errors.New("joker: credentials missing")
}
}
client := dmapi.NewClient(dmapi.AuthInfo{
APIKey: config.APIKey,
Username: config.Username,
Password: config.Password,
})
client.Debug = config.Debug
if config.HTTPClient != nil {
client.HTTPClient = config.HTTPClient
}
return &dmapiProvider{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 *dmapiProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters.
func (d *dmapiProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %w", err)
}
relative := getRelative(fqdn, zone)
if d.config.Debug {
log.Infof("[%s] joker: adding TXT record %q to zone %q with value %q", domain, relative, zone, value)
}
response, err := d.client.Login()
if err != nil {
return formatResponseError(response, err)
}
response, err = d.client.GetZone(zone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
dnsZone := dmapi.AddTxtEntryToZone(response.Body, relative, value, d.config.TTL)
response, err = d.client.PutZone(zone, dnsZone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *dmapiProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %w", err)
}
relative := getRelative(fqdn, zone)
if d.config.Debug {
log.Infof("[%s] joker: removing entry %q from zone %q", domain, relative, zone)
}
response, err := d.client.Login()
if err != nil {
return formatResponseError(response, err)
}
defer func() {
// Try to logout in case of errors
_, _ = d.client.Logout()
}()
response, err = d.client.GetZone(zone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
dnsZone, modified := dmapi.RemoveTxtEntryFromZone(response.Body, relative)
if modified {
response, err = d.client.PutZone(zone, dnsZone)
if err != nil || response.StatusCode != 0 {
return formatResponseError(response, err)
}
}
response, err = d.client.Logout()
if err != nil {
return formatResponseError(response, err)
}
return nil
}
func getRelative(fqdn, zone string) string {
return dns01.UnFqdn(strings.TrimSuffix(fqdn, dns01.ToFqdn(zone)))
}
// formatResponseError formats error with optional details from DMAPI response.
func formatResponseError(response *dmapi.Response, err error) error {
if response != nil {
return fmt.Errorf("joker: DMAPI error: %w Response: %v", err, response.Headers)
}
return fmt.Errorf("joker: DMAPI error: %w", err)
}

View file

@ -0,0 +1,129 @@
package joker
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_newDmapiProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success API key",
envVars: map[string]string{
EnvAPIKey: "123",
},
},
{
desc: "success username password",
envVars: map[string]string{
EnvUsername: "123",
EnvPassword: "123",
},
},
{
desc: "missing credentials",
envVars: map[string]string{
EnvAPIKey: "",
EnvUsername: "",
EnvPassword: "",
},
expected: "joker: some credentials information are missing: JOKER_USERNAME,JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY",
},
{
desc: "missing password",
envVars: map[string]string{
EnvAPIKey: "",
EnvUsername: "123",
EnvPassword: "",
},
expected: "joker: some credentials information are missing: JOKER_PASSWORD or some credentials information are missing: JOKER_API_KEY",
},
{
desc: "missing username",
envVars: map[string]string{
EnvAPIKey: "",
EnvUsername: "",
EnvPassword: "123",
},
expected: "joker: some credentials information are missing: JOKER_USERNAME or some credentials information are missing: JOKER_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 := newDmapiProvider()
if test.expected != "" {
require.EqualError(t, err, test.expected)
} else {
require.NoError(t, err)
require.NotNil(t, p)
assert.NotNil(t, p.config)
}
})
}
}
func Test_newDmapiProviderConfig(t *testing.T) {
testCases := []struct {
desc string
apiKey string
username string
password string
expected string
}{
{
desc: "success api key",
apiKey: "123",
},
{
desc: "success username and password",
username: "123",
password: "123",
},
{
desc: "missing credentials",
expected: "joker: credentials missing",
},
{
desc: "missing credentials: username",
expected: "joker: credentials missing",
username: "123",
},
{
desc: "missing credentials: password",
expected: "joker: credentials missing",
password: "123",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.APIKey = test.apiKey
config.Username = test.username
config.Password = test.password
p, err := newDmapiProviderConfig(config)
if test.expected != "" {
require.EqualError(t, err, test.expected)
} else {
require.NoError(t, err)
require.NotNil(t, p)
assert.NotNil(t, p.config)
}
})
}
}

View file

@ -0,0 +1,87 @@
package joker
import (
"errors"
"fmt"
"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/joker/internal/svc"
)
// svcProvider implements the challenge.Provider interface.
type svcProvider struct {
config *Config
client *svc.Client
}
// newSvcProvider returns a DNSProvider instance configured for Joker.
// Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD.
func newSvcProvider() (*svcProvider, error) {
values, err := env.Get(EnvUsername, EnvPassword)
if err != nil {
return nil, fmt.Errorf("joker: %v", err)
}
config := NewDefaultConfig()
config.Username = values[EnvUsername]
config.Password = values[EnvPassword]
return newSvcProviderConfig(config)
}
// newSvcProviderConfig return a DNSProvider instance configured for Joker.
func newSvcProviderConfig(config *Config) (*svcProvider, error) {
if config == nil {
return nil, errors.New("joker: the configuration of the DNS provider is nil")
}
if config.Username == "" || config.Password == "" {
return nil, errors.New("joker: credentials missing")
}
client := svc.NewClient(config.Username, config.Password)
return &svcProvider{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 *svcProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record using the specified parameters.
func (d *svcProvider) Present(domain, token, keyAuth string) error {
fqdn, value := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %w", err)
}
relative := getRelative(fqdn, zone)
return d.client.Send(dns01.UnFqdn(zone), relative, value)
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *svcProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _ := dns01.GetRecord(domain, keyAuth)
zone, err := dns01.FindZoneByFqdn(fqdn)
if err != nil {
return fmt.Errorf("joker: %w", err)
}
relative := getRelative(fqdn, zone)
return d.client.Send(dns01.UnFqdn(zone), relative, "")
}
// Sequential All DNS challenges for this provider will be resolved sequentially.
// Returns the interval between each iteration.
func (d *svcProvider) Sequential() time.Duration {
return d.config.SequenceInterval
}

View file

@ -0,0 +1,114 @@
package joker
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_newSvcProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success username password",
envVars: map[string]string{
EnvUsername: "123",
EnvPassword: "123",
},
},
{
desc: "missing credentials",
envVars: map[string]string{
EnvUsername: "",
EnvPassword: "",
},
expected: "joker: some credentials information are missing: JOKER_USERNAME,JOKER_PASSWORD",
},
{
desc: "missing password",
envVars: map[string]string{
EnvUsername: "123",
EnvPassword: "",
},
expected: "joker: some credentials information are missing: JOKER_PASSWORD",
},
{
desc: "missing username",
envVars: map[string]string{
EnvUsername: "",
EnvPassword: "123",
},
expected: "joker: some credentials information are missing: JOKER_USERNAME",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
envTest.Apply(test.envVars)
p, err := newSvcProvider()
if test.expected != "" {
require.EqualError(t, err, test.expected)
} else {
require.NoError(t, err)
require.NotNil(t, p)
assert.NotNil(t, p.config)
}
})
}
}
func Test_newSvcProviderConfig(t *testing.T) {
testCases := []struct {
desc string
username string
password string
expected string
}{
{
desc: "success username and password",
username: "123",
password: "123",
},
{
desc: "missing credentials",
expected: "joker: credentials missing",
},
{
desc: "missing credentials: username",
expected: "joker: credentials missing",
username: "123",
},
{
desc: "missing credentials: password",
expected: "joker: credentials missing",
password: "123",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.Username = test.username
config.Password = test.password
p, err := newSvcProviderConfig(config)
if test.expected != "" {
require.EqualError(t, err, test.expected)
} else {
require.NoError(t, err)
require.NotNil(t, p)
assert.NotNil(t, p.config)
}
})
}
}