forked from go-acme/lego
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for zone.ee as a DNS provider (go-acme#751)
- Loading branch information
Showing
6 changed files
with
684 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package zoneee | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
"path" | ||
) | ||
|
||
const defaultEndpoint = "https://api.zone.eu/v2/dns/" | ||
|
||
type txtRecord struct { | ||
// Identifier (identificator) | ||
ID string `json:"id,omitempty"` | ||
// Hostname | ||
Name string `json:"name"` | ||
// TXT content value | ||
Destination string `json:"destination"` | ||
// Can this record be deleted | ||
Delete bool `json:"delete,omitempty"` | ||
// Can this record be modified | ||
Modify bool `json:"modify,omitempty"` | ||
// API url to get this entity | ||
ResourceURL string `json:"resource_url,omitempty"` | ||
} | ||
|
||
func (d *DNSProvider) addTxtRecord(domain string, record txtRecord) ([]txtRecord, error) { | ||
reqBody := &bytes.Buffer{} | ||
if err := json.NewEncoder(reqBody).Encode(record); err != nil { | ||
return nil, err | ||
} | ||
|
||
req, err := d.makeRequest(http.MethodPost, path.Join(domain, "txt"), reqBody) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var resp []txtRecord | ||
if err := d.sendRequest(req, &resp); err != nil { | ||
return nil, err | ||
} | ||
return resp, nil | ||
} | ||
|
||
func (d *DNSProvider) getTxtRecords(domain string) ([]txtRecord, error) { | ||
req, err := d.makeRequest(http.MethodGet, path.Join(domain, "txt"), nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var resp []txtRecord | ||
if err := d.sendRequest(req, &resp); err != nil { | ||
return nil, err | ||
} | ||
return resp, nil | ||
} | ||
|
||
func (d *DNSProvider) removeTxtRecord(domain, id string) error { | ||
req, err := d.makeRequest(http.MethodDelete, path.Join(domain, "txt", id), nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return d.sendRequest(req, nil) | ||
} | ||
|
||
func (d *DNSProvider) makeRequest(method, resource string, body io.Reader) (*http.Request, error) { | ||
uri, err := d.config.Endpoint.Parse(resource) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req, err := http.NewRequest(method, uri.String(), body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req.Header.Set("Content-Type", "application/json") | ||
req.SetBasicAuth(d.config.Username, d.config.APIKey) | ||
|
||
return req, nil | ||
} | ||
|
||
func (d *DNSProvider) sendRequest(req *http.Request, result interface{}) error { | ||
resp, err := d.config.HTTPClient.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err = checkResponse(resp); err != nil { | ||
return err | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if result == nil { | ||
return nil | ||
} | ||
|
||
raw, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = json.Unmarshal(raw, result) | ||
if err != nil { | ||
return fmt.Errorf("unmarshaling %T error [status code=%d]: %v: %s", result, resp.StatusCode, err, string(raw)) | ||
} | ||
return err | ||
} | ||
|
||
func checkResponse(resp *http.Response) error { | ||
if resp.StatusCode < http.StatusBadRequest { | ||
return nil | ||
} | ||
|
||
if resp.Body == nil { | ||
return fmt.Errorf("response body is nil, status code=%d", resp.StatusCode) | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
raw, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return fmt.Errorf("unable to read body: status code=%d, error=%v", resp.StatusCode, err) | ||
} | ||
|
||
return fmt.Errorf("status code=%d: %s", resp.StatusCode, string(raw)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Package zoneee implements a DNS provider for solving the DNS-01 challenge through zone.ee. | ||
package zoneee | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"github.com/xenolf/lego/challenge/dns01" | ||
"github.com/xenolf/lego/platform/config/env" | ||
) | ||
|
||
// Config is used to configure the creation of the DNSProvider | ||
type Config struct { | ||
Endpoint *url.URL | ||
Username string | ||
APIKey string | ||
PropagationTimeout time.Duration | ||
PollingInterval time.Duration | ||
HTTPClient *http.Client | ||
} | ||
|
||
// NewDefaultConfig returns a default configuration for the DNSProvider | ||
func NewDefaultConfig() *Config { | ||
endpoint, _ := url.Parse(defaultEndpoint) | ||
|
||
return &Config{ | ||
Endpoint: endpoint, | ||
// zone.ee can take up to 5min to propagate according to the support | ||
PropagationTimeout: env.GetOrDefaultSecond("ZONEEE_PROPAGATION_TIMEOUT", 5*time.Minute), | ||
PollingInterval: env.GetOrDefaultSecond("ZONEEE_POLLING_INTERVAL", 5*time.Second), | ||
HTTPClient: &http.Client{ | ||
Timeout: env.GetOrDefaultSecond("ZONEEE_HTTP_TIMEOUT", 30*time.Second), | ||
}, | ||
} | ||
} | ||
|
||
// DNSProvider describes a provider for acme-proxy | ||
type DNSProvider struct { | ||
config *Config | ||
} | ||
|
||
// NewDNSProvider returns a DNSProvider instance. | ||
func NewDNSProvider() (*DNSProvider, error) { | ||
values, err := env.Get("ZONEEE_API_USER", "ZONEEE_API_KEY") | ||
if err != nil { | ||
return nil, fmt.Errorf("zoneee: %v", err) | ||
} | ||
|
||
rawEndpoint := env.GetOrDefaultString("ZONEEE_ENDPOINT", defaultEndpoint) | ||
endpoint, err := url.Parse(rawEndpoint) | ||
if err != nil { | ||
return nil, fmt.Errorf("zoneee: %v", err) | ||
} | ||
|
||
config := NewDefaultConfig() | ||
config.Username = values["ZONEEE_API_USER"] | ||
config.APIKey = values["ZONEEE_API_KEY"] | ||
config.Endpoint = endpoint | ||
|
||
return NewDNSProviderConfig(config) | ||
} | ||
|
||
// NewDNSProviderConfig return a DNSProvider . | ||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { | ||
if config == nil { | ||
return nil, errors.New("zoneee: the configuration of the DNS provider is nil") | ||
} | ||
|
||
if config.Username == "" { | ||
return nil, fmt.Errorf("zoneee: credentials missing: username") | ||
} | ||
|
||
if config.APIKey == "" { | ||
return nil, fmt.Errorf("zoneee: credentials missing: API key") | ||
} | ||
|
||
if config.Endpoint == nil { | ||
return nil, errors.New("zoneee: the endpoint is missing") | ||
} | ||
|
||
return &DNSProvider{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) | ||
|
||
record := txtRecord{ | ||
Name: fqdn[:len(fqdn)-1], | ||
Destination: value, | ||
} | ||
|
||
_, err := d.addTxtRecord(domain, record) | ||
if err != nil { | ||
return fmt.Errorf("zoneee: %v", err) | ||
} | ||
return nil | ||
} | ||
|
||
// CleanUp removes the TXT record previously created | ||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { | ||
_, value := dns01.GetRecord(domain, keyAuth) | ||
|
||
records, err := d.getTxtRecords(domain) | ||
if err != nil { | ||
return fmt.Errorf("zoneee: %v", err) | ||
} | ||
|
||
var id string | ||
for _, record := range records { | ||
if record.Destination == value { | ||
id = record.ID | ||
} | ||
} | ||
|
||
if id == "" { | ||
return fmt.Errorf("zoneee: txt record does not exist for %v", value) | ||
} | ||
|
||
if err = d.removeTxtRecord(domain, id); err != nil { | ||
return fmt.Errorf("zoneee: %v", err) | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.