mirror of
https://github.com/go-acme/lego.git
synced 2025-01-24 06:16:30 +02:00
add ConoHa DNS provider (#692)
This commit is contained in:
parent
2b0aa0aadf
commit
8556397ec0
@ -42,6 +42,7 @@ owners to license your work under the terms of the [MIT License](LICENSE).
|
|||||||
| Bluecat | `bluecat` | ? | - |
|
| Bluecat | `bluecat` | ? | - |
|
||||||
| Cloudflare | `cloudflare` | [documentation](https://api.cloudflare.com/) | [Go client](https://github.com/cloudflare/cloudflare-go) |
|
| Cloudflare | `cloudflare` | [documentation](https://api.cloudflare.com/) | [Go client](https://github.com/cloudflare/cloudflare-go) |
|
||||||
| CloudXNS | `cloudxns` | [documentation](https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip) | - |
|
| CloudXNS | `cloudxns` | [documentation](https://www.cloudxns.net/Public/Doc/CloudXNS_api2.0_doc_zh-cn.zip) | - |
|
||||||
|
| ConoHa | `conoha` | [documentation](https://www.conoha.jp/docs/) | - |
|
||||||
| Digital Ocean | `digitalocean` | [documentation](https://developers.digitalocean.com/documentation/v2/#domain-records) | - |
|
| Digital Ocean | `digitalocean` | [documentation](https://developers.digitalocean.com/documentation/v2/#domain-records) | - |
|
||||||
| DNSimple | `dnsimple` | [documentation](https://developer.dnsimple.com/v2/) | [Go client](https://github.com/dnsimple/dnsimple-go) |
|
| DNSimple | `dnsimple` | [documentation](https://developer.dnsimple.com/v2/) | [Go client](https://github.com/dnsimple/dnsimple-go) |
|
||||||
| DNS Made Easy | `dnsmadeeasy` | [documentation](https://api-docs.dnsmadeeasy.com/) | - |
|
| DNS Made Easy | `dnsmadeeasy` | [documentation](https://api-docs.dnsmadeeasy.com/) | - |
|
||||||
|
2
cli.go
2
cli.go
@ -209,6 +209,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
|||||||
fmt.Fprintln(w, "\tbluecat:\tBLUECAT_SERVER_URL, BLUECAT_USER_NAME, BLUECAT_PASSWORD, BLUECAT_CONFIG_NAME, BLUECAT_DNS_VIEW")
|
fmt.Fprintln(w, "\tbluecat:\tBLUECAT_SERVER_URL, BLUECAT_USER_NAME, BLUECAT_PASSWORD, BLUECAT_CONFIG_NAME, BLUECAT_DNS_VIEW")
|
||||||
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
|
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY")
|
||||||
fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_API_KEY, CLOUDXNS_SECRET_KEY")
|
fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_API_KEY, CLOUDXNS_SECRET_KEY")
|
||||||
|
fmt.Fprintln(w, "\tconoha:\tCONOHA_REGION, CONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD")
|
||||||
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
|
fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN")
|
||||||
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN")
|
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN")
|
||||||
fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET")
|
fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET")
|
||||||
@ -254,6 +255,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
|||||||
fmt.Fprintln(w, "\tbluecat:\tBLUECAT_POLLING_INTERVAL, BLUECAT_PROPAGATION_TIMEOUT, BLUECAT_TTL, BLUECAT_HTTP_TIMEOUT")
|
fmt.Fprintln(w, "\tbluecat:\tBLUECAT_POLLING_INTERVAL, BLUECAT_PROPAGATION_TIMEOUT, BLUECAT_TTL, BLUECAT_HTTP_TIMEOUT")
|
||||||
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_POLLING_INTERVAL, CLOUDFLARE_PROPAGATION_TIMEOUT, CLOUDFLARE_TTL, CLOUDFLARE_HTTP_TIMEOUT")
|
fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_POLLING_INTERVAL, CLOUDFLARE_PROPAGATION_TIMEOUT, CLOUDFLARE_TTL, CLOUDFLARE_HTTP_TIMEOUT")
|
||||||
fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_POLLING_INTERVAL, CLOUDXNS_PROPAGATION_TIMEOUT, CLOUDXNS_TTL, CLOUDXNS_HTTP_TIMEOUT")
|
fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_POLLING_INTERVAL, CLOUDXNS_PROPAGATION_TIMEOUT, CLOUDXNS_TTL, CLOUDXNS_HTTP_TIMEOUT")
|
||||||
|
fmt.Fprintln(w, "\tconoha:\tCONOHA_POLLING_INTERVAL, CONOHA_PROPAGATION_TIMEOUT, CONOHA_TTL, CONOHA_HTTP_TIMEOUT")
|
||||||
fmt.Fprintln(w, "\tdigitalocean:\tDO_POLLING_INTERVAL, DO_PROPAGATION_TIMEOUT, DO_TTL, DO_HTTP_TIMEOUT")
|
fmt.Fprintln(w, "\tdigitalocean:\tDO_POLLING_INTERVAL, DO_PROPAGATION_TIMEOUT, DO_TTL, DO_HTTP_TIMEOUT")
|
||||||
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_TTL, DNSIMPLE_POLLING_INTERVAL, DNSIMPLE_PROPAGATION_TIMEOUT")
|
fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_TTL, DNSIMPLE_POLLING_INTERVAL, DNSIMPLE_PROPAGATION_TIMEOUT")
|
||||||
fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_POLLING_INTERVAL, DNSMADEEASY_PROPAGATION_TIMEOUT, DNSMADEEASY_TTL, DNSMADEEASY_HTTP_TIMEOUT")
|
fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_POLLING_INTERVAL, DNSMADEEASY_PROPAGATION_TIMEOUT, DNSMADEEASY_TTL, DNSMADEEASY_HTTP_TIMEOUT")
|
||||||
|
205
providers/dns/conoha/client.go
Normal file
205
providers/dns/conoha/client.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package conoha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
identityBaseURL = "https://identity.%s.conoha.io"
|
||||||
|
dnsServiceBaseURL = "https://dns-service.%s.conoha.io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IdentityRequest is an authentication request body.
|
||||||
|
type IdentityRequest struct {
|
||||||
|
Auth Auth `json:"auth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth is an authentication information.
|
||||||
|
type Auth struct {
|
||||||
|
TenantID string `json:"tenantId"`
|
||||||
|
PasswordCredentials PasswordCredentials `json:"passwordCredentials"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasswordCredentials is API-user's credentials.
|
||||||
|
type PasswordCredentials struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdentityResponse is an authentication response body.
|
||||||
|
type IdentityResponse struct {
|
||||||
|
Access Access `json:"access"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access is an identity information.
|
||||||
|
type Access struct {
|
||||||
|
Token Token `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token is an api access token.
|
||||||
|
type Token struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DomainListResponse is a response of a domain listing request.
|
||||||
|
type DomainListResponse struct {
|
||||||
|
Domains []Domain `json:"domains"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain is a hosted domain entry.
|
||||||
|
type Domain struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordListResponse is a response of record listing request.
|
||||||
|
type RecordListResponse struct {
|
||||||
|
Records []Record `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record is a record entry.
|
||||||
|
type Record struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is a ConoHa API client.
|
||||||
|
type Client struct {
|
||||||
|
token string
|
||||||
|
endpoint string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient returns a client instance logged into the ConoHa service.
|
||||||
|
func NewClient(region string, auth Auth, httpClient *http.Client) (*Client, error) {
|
||||||
|
if httpClient == nil {
|
||||||
|
httpClient = &http.Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{httpClient: httpClient}
|
||||||
|
|
||||||
|
c.endpoint = fmt.Sprintf(identityBaseURL, region)
|
||||||
|
|
||||||
|
identity, err := c.getIdentity(auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.token = identity.Access.Token.ID
|
||||||
|
c.endpoint = fmt.Sprintf(dnsServiceBaseURL, region)
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getIdentity(auth Auth) (*IdentityResponse, error) {
|
||||||
|
req := &IdentityRequest{Auth: auth}
|
||||||
|
|
||||||
|
identity := &IdentityResponse{}
|
||||||
|
|
||||||
|
err := c.do(http.MethodPost, "/v2.0/tokens", req, identity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return identity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomainID returns an ID of specified domain.
|
||||||
|
func (c *Client) GetDomainID(domainName string) (string, error) {
|
||||||
|
domainList := &DomainListResponse{}
|
||||||
|
|
||||||
|
err := c.do(http.MethodGet, "/v1/domains", nil, domainList)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range domainList.Domains {
|
||||||
|
if domain.Name == domainName {
|
||||||
|
return domain.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no such domain: %s", domainName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRecordID returns an ID of specified record.
|
||||||
|
func (c *Client) GetRecordID(domainID, recordName, recordType, data string) (string, error) {
|
||||||
|
recordList := &RecordListResponse{}
|
||||||
|
|
||||||
|
err := c.do(http.MethodGet, fmt.Sprintf("/v1/domains/%s/records", domainID), nil, recordList)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range recordList.Records {
|
||||||
|
if record.Name == recordName && record.Type == recordType && record.Data == data {
|
||||||
|
return record.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("no such record")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRecord adds new record.
|
||||||
|
func (c *Client) CreateRecord(domainID string, record Record) error {
|
||||||
|
return c.do(http.MethodPost, fmt.Sprintf("/v1/domains/%s/records", domainID), record, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRecord removes specified record.
|
||||||
|
func (c *Client) DeleteRecord(domainID, recordID string) error {
|
||||||
|
return c.do(http.MethodDelete, fmt.Sprintf("/v1/domains/%s/records/%s", domainID, recordID), nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) do(method, path string, payload, result interface{}) error {
|
||||||
|
body := bytes.NewReader(nil)
|
||||||
|
|
||||||
|
if payload != nil {
|
||||||
|
bodyBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body = bytes.NewReader(bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, c.endpoint+path, body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Auth-Token", c.token)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return fmt.Errorf("HTTP request failed with status code %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != nil {
|
||||||
|
respBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return json.Unmarshal(respBody, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
210
providers/dns/conoha/client_test.go
Normal file
210
providers/dns/conoha/client_test.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
package conoha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupClientTest() (*http.ServeMux, *Client, func()) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
token: "secret",
|
||||||
|
endpoint: server.URL,
|
||||||
|
httpClient: &http.Client{Timeout: 5 * time.Second},
|
||||||
|
}
|
||||||
|
|
||||||
|
return mux, client, server.Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetDomainID(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
domainID string
|
||||||
|
error bool
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
domainName string
|
||||||
|
handler http.HandlerFunc
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
domainName: "domain1.com.",
|
||||||
|
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := `
|
||||||
|
{
|
||||||
|
"domains":[
|
||||||
|
{
|
||||||
|
"id": "09494b72-b65b-4297-9efb-187f65a0553e",
|
||||||
|
"name": "domain1.com.",
|
||||||
|
"ttl": 3600,
|
||||||
|
"serial": 1351800668,
|
||||||
|
"email": "nsadmin@example.org",
|
||||||
|
"gslb": 0,
|
||||||
|
"created_at": "2012-11-01T20:11:08.000000",
|
||||||
|
"updated_at": null,
|
||||||
|
"description": "memo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cf661142-e577-40b5-b3eb-75795cdc0cd7",
|
||||||
|
"name": "domain2.com.",
|
||||||
|
"ttl": 7200,
|
||||||
|
"serial": 1351800670,
|
||||||
|
"email": "nsadmin2@example.org",
|
||||||
|
"gslb": 1,
|
||||||
|
"created_at": "2012-11-01T20:11:08.000000",
|
||||||
|
"updated_at": "2012-12-01T20:11:08.000000",
|
||||||
|
"description": "memomemo"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
_, err := fmt.Fprint(rw, content)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expected: expected{domainID: "09494b72-b65b-4297-9efb-187f65a0553e"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "non existing domain",
|
||||||
|
domainName: "domain1.com.",
|
||||||
|
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := fmt.Fprint(rw, "{}")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expected: expected{error: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "marshaling error",
|
||||||
|
domainName: "domain1.com.",
|
||||||
|
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := fmt.Fprint(rw, "[]")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expected: expected{error: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
mux, client, tearDown := setupClientTest()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.Handle("/v1/domains", test.handler)
|
||||||
|
|
||||||
|
domainID, err := client.GetDomainID(test.domainName)
|
||||||
|
|
||||||
|
if test.expected.error {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected.domainID, domainID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_CreateRecord(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
handler http.HandlerFunc
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer req.Body.Close()
|
||||||
|
|
||||||
|
if string(raw) != `{"name":"lego.com.","type":"TXT","data":"txtTXTtxt","ttl":300}` {
|
||||||
|
http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bad request",
|
||||||
|
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
http.Error(rw, fmt.Sprintf("%s: %s", http.StatusText(http.StatusMethodNotAllowed), req.Method), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(rw, "OOPS", http.StatusBadRequest)
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
mux, client, tearDown := setupClientTest()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.Handle("/v1/domains/lego/records", test.handler)
|
||||||
|
|
||||||
|
domainID := "lego"
|
||||||
|
|
||||||
|
record := Record{
|
||||||
|
Name: "lego.com.",
|
||||||
|
Type: "TXT",
|
||||||
|
Data: "txtTXTtxt",
|
||||||
|
TTL: 300,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.CreateRecord(domainID, record)
|
||||||
|
|
||||||
|
if test.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
138
providers/dns/conoha/conoha.go
Normal file
138
providers/dns/conoha/conoha.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// Package conoha implements a DNS provider for solving the DNS-01 challenge
|
||||||
|
// using ConoHa DNS.
|
||||||
|
package conoha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
"github.com/xenolf/lego/platform/config/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is used to configure the creation of the DNSProvider
|
||||||
|
type Config struct {
|
||||||
|
Region string
|
||||||
|
TenantID string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
TTL int
|
||||||
|
PropagationTimeout time.Duration
|
||||||
|
PollingInterval time.Duration
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a default configuration for the DNSProvider
|
||||||
|
func NewDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Region: env.GetOrDefaultString("CONOHA_REGION", "tyo1"),
|
||||||
|
TTL: env.GetOrDefaultInt("CONOHA_TTL", 60),
|
||||||
|
PropagationTimeout: env.GetOrDefaultSecond("CONOHA_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
|
||||||
|
PollingInterval: env.GetOrDefaultSecond("CONOHA_POLLING_INTERVAL", acme.DefaultPollingInterval),
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Timeout: env.GetOrDefaultSecond("CONOHA_HTTP_TIMEOUT", 30*time.Second),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
||||||
|
type DNSProvider struct {
|
||||||
|
config *Config
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS.
|
||||||
|
// Credentials must be passed in the environment variables: CONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
values, err := env.Get("CONOHA_TENANT_ID", "CONOHA_API_USERNAME", "CONOHA_API_PASSWORD")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("conoha: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.TenantID = values["CONOHA_TENANT_ID"]
|
||||||
|
config.Username = values["CONOHA_API_USERNAME"]
|
||||||
|
config.Password = values["CONOHA_API_PASSWORD"]
|
||||||
|
|
||||||
|
return NewDNSProviderConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderConfig return a DNSProvider instance configured for ConoHa DNS.
|
||||||
|
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("conoha: the configuration of the DNS provider is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.TenantID == "" || config.Username == "" || config.Password == "" {
|
||||||
|
return nil, errors.New("conoha: some credentials information are missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := Auth{
|
||||||
|
TenantID: config.TenantID,
|
||||||
|
PasswordCredentials: PasswordCredentials{
|
||||||
|
Username: config.Username,
|
||||||
|
Password: config.Password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewClient(config.Region, auth, config.HTTPClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("conoha: failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{config: config, client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
|
id, err := d.client.GetDomainID(acme.ToFqdn(domain))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("conoha: failed to get domain ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := Record{
|
||||||
|
Name: fqdn,
|
||||||
|
Type: "TXT",
|
||||||
|
Data: value,
|
||||||
|
TTL: d.config.TTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.client.CreateRecord(id, record)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("conoha: failed to create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp clears ConoHa DNS TXT record
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
|
domID, err := d.client.GetDomainID(acme.ToFqdn(domain))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("conoha: failed to get domain ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
recID, err := d.client.GetRecordID(domID, fqdn, "TXT", value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("conoha: failed to get record ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.client.DeleteRecord(domID, recID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("conoha: failed to delete record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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
|
||||||
|
}
|
176
providers/dns/conoha/conoha_test.go
Normal file
176
providers/dns/conoha/conoha_test.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package conoha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/xenolf/lego/platform/tester"
|
||||||
|
)
|
||||||
|
|
||||||
|
var envTest = tester.NewEnvTest(
|
||||||
|
"CONOHA_TENANT_ID",
|
||||||
|
"CONOHA_API_USERNAME",
|
||||||
|
"CONOHA_API_PASSWORD").
|
||||||
|
WithDomain("CONOHA_DOMAIN")
|
||||||
|
|
||||||
|
func TestNewDNSProvider(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
envVars map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "complete credentials, but login failed",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"CONOHA_TENANT_ID": "tenant_id",
|
||||||
|
"CONOHA_API_USERNAME": "api_username",
|
||||||
|
"CONOHA_API_PASSWORD": "api_password",
|
||||||
|
},
|
||||||
|
expected: `conoha: failed to create client: failed to login: HTTP request failed with status code 401: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing credentials",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"CONOHA_TENANT_ID": "",
|
||||||
|
"CONOHA_API_USERNAME": "",
|
||||||
|
"CONOHA_API_PASSWORD": "",
|
||||||
|
},
|
||||||
|
expected: "conoha: some credentials information are missing: CONOHA_TENANT_ID,CONOHA_API_USERNAME,CONOHA_API_PASSWORD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing tenant id",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"CONOHA_TENANT_ID": "",
|
||||||
|
"CONOHA_API_USERNAME": "api_username",
|
||||||
|
"CONOHA_API_PASSWORD": "api_password",
|
||||||
|
},
|
||||||
|
expected: "conoha: some credentials information are missing: CONOHA_TENANT_ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing api username",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"CONOHA_TENANT_ID": "tenant_id",
|
||||||
|
"CONOHA_API_USERNAME": "",
|
||||||
|
"CONOHA_API_PASSWORD": "api_password",
|
||||||
|
},
|
||||||
|
expected: "conoha: some credentials information are missing: CONOHA_API_USERNAME",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing api password",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"CONOHA_TENANT_ID": "tenant_id",
|
||||||
|
"CONOHA_API_USERNAME": "api_username",
|
||||||
|
"CONOHA_API_PASSWORD": "",
|
||||||
|
},
|
||||||
|
expected: "conoha: some credentials information are missing: CONOHA_API_PASSWORD",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
tenant string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "complete credentials, but login failed",
|
||||||
|
expected: `conoha: failed to create client: failed to login: HTTP request failed with status code 401: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`,
|
||||||
|
tenant: "tenant_id",
|
||||||
|
username: "api_username",
|
||||||
|
password: "api_password",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing credentials",
|
||||||
|
expected: "conoha: some credentials information are missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing tenant id",
|
||||||
|
expected: "conoha: some credentials information are missing",
|
||||||
|
username: "api_username",
|
||||||
|
password: "api_password",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing api username",
|
||||||
|
expected: "conoha: some credentials information are missing",
|
||||||
|
tenant: "tenant_id",
|
||||||
|
password: "api_password",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing api password",
|
||||||
|
expected: "conoha: some credentials information are missing",
|
||||||
|
tenant: "tenant_id",
|
||||||
|
username: "api_username",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.TenantID = test.tenant
|
||||||
|
config.Username = test.username
|
||||||
|
config.Password = test.password
|
||||||
|
|
||||||
|
p, err := NewDNSProviderConfig(config)
|
||||||
|
|
||||||
|
if len(test.expected) == 0 {
|
||||||
|
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)
|
||||||
|
}
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/xenolf/lego/providers/dns/bluecat"
|
"github.com/xenolf/lego/providers/dns/bluecat"
|
||||||
"github.com/xenolf/lego/providers/dns/cloudflare"
|
"github.com/xenolf/lego/providers/dns/cloudflare"
|
||||||
"github.com/xenolf/lego/providers/dns/cloudxns"
|
"github.com/xenolf/lego/providers/dns/cloudxns"
|
||||||
|
"github.com/xenolf/lego/providers/dns/conoha"
|
||||||
"github.com/xenolf/lego/providers/dns/digitalocean"
|
"github.com/xenolf/lego/providers/dns/digitalocean"
|
||||||
"github.com/xenolf/lego/providers/dns/dnsimple"
|
"github.com/xenolf/lego/providers/dns/dnsimple"
|
||||||
"github.com/xenolf/lego/providers/dns/dnsmadeeasy"
|
"github.com/xenolf/lego/providers/dns/dnsmadeeasy"
|
||||||
@ -65,6 +66,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
|||||||
return cloudflare.NewDNSProvider()
|
return cloudflare.NewDNSProvider()
|
||||||
case "cloudxns":
|
case "cloudxns":
|
||||||
return cloudxns.NewDNSProvider()
|
return cloudxns.NewDNSProvider()
|
||||||
|
case "conoha":
|
||||||
|
return conoha.NewDNSProvider()
|
||||||
case "digitalocean":
|
case "digitalocean":
|
||||||
return digitalocean.NewDNSProvider()
|
return digitalocean.NewDNSProvider()
|
||||||
case "dnsimple":
|
case "dnsimple":
|
||||||
|
Loading…
x
Reference in New Issue
Block a user