-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
468 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package safedns | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
"github.com/go-acme/lego/v4/challenge/dns01" | ||
"github.com/go-acme/lego/v4/log" | ||
) | ||
|
||
const defaultBaseURL = "https://api.ukfast.io" | ||
|
||
type txtResponse struct { | ||
Data struct { | ||
ID int `json:"id"` | ||
} `json:"data"` | ||
Meta struct { | ||
Location string `json:"location"` | ||
} | ||
} | ||
|
||
type recordType string | ||
|
||
const ( | ||
typeTXT recordType = "TXT" | ||
) | ||
|
||
type record struct { | ||
Name string `json:"name"` | ||
Type recordType `json:"type"` | ||
Content string `json:"content"` | ||
TTL int `json:"ttl"` | ||
} | ||
|
||
type apiError struct { | ||
Message string `json:"message"` | ||
} | ||
|
||
func (d *DNSProvider) addTxtRecord(fqdn, value string) (*txtResponse, error) { | ||
zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(fqdn)) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not determine zone for domain: %q: %w", fqdn, err) | ||
} | ||
|
||
reqData := record{Name: dns01.UnFqdn(fqdn), Type: typeTXT, Content: fmt.Sprintf("\"%s\"", value), TTL: d.config.TTL} | ||
body, err := json.Marshal(reqData) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
url := fmt.Sprintf("%s/safedns/v1/zones/%s/records", d.config.BaseURL, dns01.UnFqdn(zone)) | ||
req, err := d.newRequest(http.MethodPost, url, bytes.NewReader(body)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
log.Infof("safedns: creating record %+v at %s", reqData, url) | ||
|
||
resp, err := d.config.HTTPClient.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode >= http.StatusBadRequest { | ||
return nil, readError(req, resp) | ||
} | ||
|
||
content, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, errors.New(toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
respData := &txtResponse{} | ||
err = json.Unmarshal(content, respData) | ||
if err != nil { | ||
return nil, fmt.Errorf("%w: %s", err, toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
log.Infof("safedns: created record with ID %d", respData.Data.ID) | ||
|
||
return respData, nil | ||
} | ||
|
||
func (d *DNSProvider) removeTxtRecord(domain string, recordID int) error { | ||
authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) | ||
if err != nil { | ||
return fmt.Errorf("safedns: could not determine zone for domain %q: %w", domain, err) | ||
} | ||
|
||
reqURL := fmt.Sprintf("%s/safedns/v1/zones/%s/records/%d", d.config.BaseURL, dns01.UnFqdn(authZone), recordID) | ||
req, err := d.newRequest(http.MethodDelete, reqURL, nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
log.Infof("safedns: cleaning up record %d at %s", recordID, reqURL) | ||
|
||
resp, err := d.config.HTTPClient.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode >= 400 { | ||
return readError(req, resp) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (d *DNSProvider) newRequest(method, url string, body io.Reader) (*http.Request, error) { | ||
req, err := http.NewRequest(method, url, body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("Accept", "application/json") | ||
req.Header.Set("Authorization", d.config.AuthToken) | ||
|
||
return req, nil | ||
} | ||
|
||
func readError(req *http.Request, resp *http.Response) error { | ||
content, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return errors.New(toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
var errInfo apiError | ||
err = json.Unmarshal(content, &errInfo) | ||
if err != nil { | ||
return fmt.Errorf("safedns: unmarshaling error: %w: %s", err, toUnreadableBodyMessage(req, content)) | ||
} | ||
|
||
return fmt.Errorf("safedns: HTTP %d: %s", resp.StatusCode, content) | ||
} | ||
|
||
func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { | ||
return fmt.Sprintf("the request %s received a response with an invalid format: %q", req.URL, string(rawBody)) | ||
} |
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,127 @@ | ||
package safedns | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"sync" | ||
"time" | ||
|
||
"github.com/go-acme/lego/v4/challenge/dns01" | ||
"github.com/go-acme/lego/v4/platform/config/env" | ||
) | ||
|
||
// Environment variables. | ||
const ( | ||
envNamespace = "SAFEDNS_" | ||
|
||
EnvAuthToken = envNamespace + "AUTH_TOKEN" | ||
EnvTTL = envNamespace + "TTL" | ||
EnvAPITimeout = envNamespace + "API_TIMEOUT" | ||
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" | ||
EnvPollingInterval = envNamespace + "POLLING_INTERVAL" | ||
) | ||
|
||
type Config struct { | ||
BaseURL string | ||
AuthToken string | ||
TTL int | ||
APITimeout time.Duration | ||
PropagationTimeout time.Duration | ||
PollingInterval time.Duration | ||
HTTPClient *http.Client | ||
} | ||
|
||
func NewDefaultConfig() *Config { | ||
return &Config{ | ||
BaseURL: defaultBaseURL, | ||
TTL: env.GetOrDefaultInt(EnvTTL, 30), | ||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 60*time.Second), | ||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), | ||
HTTPClient: &http.Client{ | ||
Timeout: env.GetOrDefaultSecond(EnvAPITimeout, 30*time.Second), | ||
}, | ||
} | ||
} | ||
|
||
type DNSProvider struct { | ||
config *Config | ||
recordIDs map[string]int | ||
recordIDsMu sync.Mutex | ||
} | ||
|
||
func NewDNSProvider() (*DNSProvider, error) { | ||
values, err := env.Get(EnvAuthToken) | ||
if err != nil { | ||
return nil, fmt.Errorf("safedns: %w", err) | ||
} | ||
|
||
config := NewDefaultConfig() | ||
config.AuthToken = values[EnvAuthToken] | ||
return NewDNSProviderConfig(config) | ||
} | ||
|
||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { | ||
if config == nil { | ||
return nil, errors.New("safedns: supplied configuration was nil") | ||
} | ||
|
||
if config.AuthToken == "" { | ||
return nil, errors.New("safedns: credentials missing") | ||
} | ||
|
||
if config.BaseURL == "" { | ||
config.BaseURL = defaultBaseURL | ||
} | ||
|
||
return &DNSProvider{ | ||
config: config, | ||
recordIDs: make(map[string]int), | ||
}, nil | ||
} | ||
|
||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { | ||
return d.config.PropagationTimeout, d.config.PollingInterval | ||
} | ||
|
||
func (d *DNSProvider) Present(domain, token, keyAuth string) error { | ||
fqdn, value := dns01.GetRecord(domain, keyAuth) | ||
|
||
respData, err := d.addTxtRecord(fqdn, value) | ||
if err != nil { | ||
return fmt.Errorf("safedns: %w", err) | ||
} | ||
|
||
d.recordIDsMu.Lock() | ||
d.recordIDs[token] = respData.Data.ID | ||
d.recordIDsMu.Unlock() | ||
|
||
return nil | ||
} | ||
|
||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { | ||
fqdn, _ := dns01.GetRecord(domain, keyAuth) | ||
|
||
authZone, err := dns01.FindZoneByFqdn(fqdn) | ||
if err != nil { | ||
return fmt.Errorf("safedns: %w", err) | ||
} | ||
|
||
d.recordIDsMu.Lock() | ||
recordID, ok := d.recordIDs[token] | ||
d.recordIDsMu.Unlock() | ||
if !ok { | ||
return fmt.Errorf("safedns: unknown record ID for '%s'", fqdn) | ||
} | ||
|
||
err = d.removeTxtRecord(authZone, recordID) | ||
if err != nil { | ||
return fmt.Errorf("safedns: %w", err) | ||
} | ||
|
||
d.recordIDsMu.Lock() | ||
delete(d.recordIDs, token) | ||
d.recordIDsMu.Unlock() | ||
|
||
return nil | ||
} |
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,22 @@ | ||
Name = "SafeDNS" | ||
Description = '''''' | ||
URL = "https://www.ukfast.co.uk/dns-hosting.html" | ||
Code = "safedns" | ||
Since = "v4.6.0" | ||
|
||
Example = ''' | ||
SAFEDNS_AUTH_TOKEN=xxxxxx \ | ||
lego --email [email protected] --dns safedns --domains my.example.org run | ||
''' | ||
|
||
[Configuration] | ||
[Configuration.Credentials] | ||
SAFEDNS_AUTH_TOKEN = "Authentication token" | ||
[Configuration.Additional] | ||
SAFEDNS_TTL = "TXT record TTL" | ||
SAFEDNS_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" | ||
SAFEDNS_API_TIMEOUT = "API request timeout in seconds" | ||
SAFEDNS_POLLING_INTERVAL = "Time to wait for initial check" | ||
|
||
[Links] | ||
API = "https://developers.ukfast.io/documentation/safedns" |
Oops, something went wrong.