forked from TrueCloudLab/lego
Add DNS Provider for Stackpath (#660)
This commit is contained in:
parent
dd087560a0
commit
8a5cdcb4ef
8 changed files with 801 additions and 1 deletions
|
@ -74,5 +74,6 @@ owners to license your work under the terms of the [MIT License](LICENSE).
|
||||||
| RFC2136 | `rfc2136` | [documentation](https://tools.ietf.org/html/rfc2136) | - |
|
| RFC2136 | `rfc2136` | [documentation](https://tools.ietf.org/html/rfc2136) | - |
|
||||||
| Route 53 | `route53` | [documentation](https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html) | [Go client](https://github.com/aws/aws-sdk-go/aws) |
|
| Route 53 | `route53` | [documentation](https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html) | [Go client](https://github.com/aws/aws-sdk-go/aws) |
|
||||||
| Sakura Cloud | `sakuracloud` | [documentation](https://developer.sakura.ad.jp/cloud/api/1.1/) | [Go client](https://github.com/sacloud/libsacloud) |
|
| Sakura Cloud | `sakuracloud` | [documentation](https://developer.sakura.ad.jp/cloud/api/1.1/) | [Go client](https://github.com/sacloud/libsacloud) |
|
||||||
|
| Stackpath | `stackpath` | [documentation](https://developer.stackpath.com/en/api/dns/#tag/Zone) | - |
|
||||||
| VegaDNS | `vegadns` | [documentation](https://github.com/shupp/VegaDNS-API) | [Go client](https://github.com/OpenDNS/vegadns2client) |
|
| VegaDNS | `vegadns` | [documentation](https://github.com/shupp/VegaDNS-API) | [Go client](https://github.com/OpenDNS/vegadns2client) |
|
||||||
| Vultr | `vultr` | [documentation](https://www.vultr.com/api/#dns) | [Go client](https://github.com/JamesClonk/vultr) |
|
| Vultr | `vultr` | [documentation](https://www.vultr.com/api/#dns) | [Go client](https://github.com/JamesClonk/vultr) |
|
||||||
|
|
5
Gopkg.lock
generated
5
Gopkg.lock
generated
|
@ -445,10 +445,11 @@
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:bc2b221d465bb28ce46e8d472ecdc424b9a9b541bd61d8c311c5f29c8dd75b1b"
|
digest = "1:b20a60bb1085d4c535af064faf3e74b4e185781b58bba1ad7406cd9733d82403"
|
||||||
name = "golang.org/x/oauth2"
|
name = "golang.org/x/oauth2"
|
||||||
packages = [
|
packages = [
|
||||||
".",
|
".",
|
||||||
|
"clientcredentials",
|
||||||
"google",
|
"google",
|
||||||
"internal",
|
"internal",
|
||||||
"jws",
|
"jws",
|
||||||
|
@ -615,7 +616,9 @@
|
||||||
"github.com/urfave/cli",
|
"github.com/urfave/cli",
|
||||||
"golang.org/x/crypto/ocsp",
|
"golang.org/x/crypto/ocsp",
|
||||||
"golang.org/x/net/context",
|
"golang.org/x/net/context",
|
||||||
|
"golang.org/x/net/publicsuffix",
|
||||||
"golang.org/x/oauth2",
|
"golang.org/x/oauth2",
|
||||||
|
"golang.org/x/oauth2/clientcredentials",
|
||||||
"golang.org/x/oauth2/google",
|
"golang.org/x/oauth2/google",
|
||||||
"google.golang.org/api/dns/v1",
|
"google.golang.org/api/dns/v1",
|
||||||
"gopkg.in/ns1/ns1-go.v2/rest",
|
"gopkg.in/ns1/ns1-go.v2/rest",
|
||||||
|
|
2
cli.go
2
cli.go
|
@ -241,6 +241,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
||||||
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
|
fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER")
|
||||||
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID")
|
fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID")
|
||||||
fmt.Fprintln(w, "\tsakuracloud:\tSAKURACLOUD_ACCESS_TOKEN, SAKURACLOUD_ACCESS_TOKEN_SECRET")
|
fmt.Fprintln(w, "\tsakuracloud:\tSAKURACLOUD_ACCESS_TOKEN, SAKURACLOUD_ACCESS_TOKEN_SECRET")
|
||||||
|
fmt.Fprintln(w, "\tstackpath:\tSTACKPATH_CLIENT_ID, STACKPATH_CLIENT_SECRET, STACKPATH_STACK_ID")
|
||||||
fmt.Fprintln(w, "\tvegadns:\tSECRET_VEGADNS_KEY, SECRET_VEGADNS_SECRET, VEGADNS_URL")
|
fmt.Fprintln(w, "\tvegadns:\tSECRET_VEGADNS_KEY, SECRET_VEGADNS_SECRET, VEGADNS_URL")
|
||||||
fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY")
|
fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY")
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
|
@ -282,6 +283,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
||||||
fmt.Fprintln(w, "\trfc2136:\tRFC2136_POLLING_INTERVAL, RFC2136_PROPAGATION_TIMEOUT, RFC2136_TTL")
|
fmt.Fprintln(w, "\trfc2136:\tRFC2136_POLLING_INTERVAL, RFC2136_PROPAGATION_TIMEOUT, RFC2136_TTL")
|
||||||
fmt.Fprintln(w, "\troute53:\tAWS_POLLING_INTERVAL, AWS_PROPAGATION_TIMEOUT, AWS_TTL")
|
fmt.Fprintln(w, "\troute53:\tAWS_POLLING_INTERVAL, AWS_PROPAGATION_TIMEOUT, AWS_TTL")
|
||||||
fmt.Fprintln(w, "\tsakuracloud:\tSAKURACLOUD_POLLING_INTERVAL, SAKURACLOUD_PROPAGATION_TIMEOUT, SAKURACLOUD_TTL")
|
fmt.Fprintln(w, "\tsakuracloud:\tSAKURACLOUD_POLLING_INTERVAL, SAKURACLOUD_PROPAGATION_TIMEOUT, SAKURACLOUD_TTL")
|
||||||
|
fmt.Fprintln(w, "\tstackpath:\tSTACKPATH_POLLING_INTERVAL, STACKPATH_PROPAGATION_TIMEOUT, STACKPATH_TTL")
|
||||||
fmt.Fprintln(w, "\tvegadns:\tVEGADNS_POLLING_INTERVAL, VEGADNS_PROPAGATION_TIMEOUT, VEGADNS_TTL")
|
fmt.Fprintln(w, "\tvegadns:\tVEGADNS_POLLING_INTERVAL, VEGADNS_PROPAGATION_TIMEOUT, VEGADNS_TTL")
|
||||||
fmt.Fprintln(w, "\tvultr:\tVULTR_POLLING_INTERVAL, VULTR_PROPAGATION_TIMEOUT, VULTR_TTL, VULTR_HTTP_TIMEOUT")
|
fmt.Fprintln(w, "\tvultr:\tVULTR_POLLING_INTERVAL, VULTR_PROPAGATION_TIMEOUT, VULTR_TTL, VULTR_HTTP_TIMEOUT")
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ import (
|
||||||
"github.com/xenolf/lego/providers/dns/rfc2136"
|
"github.com/xenolf/lego/providers/dns/rfc2136"
|
||||||
"github.com/xenolf/lego/providers/dns/route53"
|
"github.com/xenolf/lego/providers/dns/route53"
|
||||||
"github.com/xenolf/lego/providers/dns/sakuracloud"
|
"github.com/xenolf/lego/providers/dns/sakuracloud"
|
||||||
|
"github.com/xenolf/lego/providers/dns/stackpath"
|
||||||
"github.com/xenolf/lego/providers/dns/vegadns"
|
"github.com/xenolf/lego/providers/dns/vegadns"
|
||||||
"github.com/xenolf/lego/providers/dns/vultr"
|
"github.com/xenolf/lego/providers/dns/vultr"
|
||||||
)
|
)
|
||||||
|
@ -127,6 +128,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
||||||
return rfc2136.NewDNSProvider()
|
return rfc2136.NewDNSProvider()
|
||||||
case "sakuracloud":
|
case "sakuracloud":
|
||||||
return sakuracloud.NewDNSProvider()
|
return sakuracloud.NewDNSProvider()
|
||||||
|
case "stackpath":
|
||||||
|
return stackpath.NewDNSProvider()
|
||||||
case "vegadns":
|
case "vegadns":
|
||||||
return vegadns.NewDNSProvider()
|
return vegadns.NewDNSProvider()
|
||||||
case "vultr":
|
case "vultr":
|
||||||
|
|
217
providers/dns/stackpath/client.go
Normal file
217
providers/dns/stackpath/client.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package stackpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Zones is the response struct from the Stackpath api GetZones
|
||||||
|
type Zones struct {
|
||||||
|
Zones []Zone `json:"zones"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone a DNS zone representation
|
||||||
|
type Zone struct {
|
||||||
|
ID string
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Records is the response struct from the Stackpath api GetZoneRecords
|
||||||
|
type Records struct {
|
||||||
|
Records []Record `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record a DNS record representation
|
||||||
|
type Record struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorResponse the API error response representation
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorResponse) Error() string {
|
||||||
|
return fmt.Sprintf("%d %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.stackpath.com/en/api/dns/#operation/GetZones
|
||||||
|
func (d *DNSProvider) getZones(domain string) (*Zone, error) {
|
||||||
|
domain = acme.UnFqdn(domain)
|
||||||
|
tld, err := publicsuffix.EffectiveTLDPlusOne(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := d.newRequest(http.MethodGet, "/zones", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Add("page_request.filter", fmt.Sprintf("domain='%s'", tld))
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
var zones Zones
|
||||||
|
err = d.do(req, &zones)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(zones.Zones) == 0 {
|
||||||
|
return nil, fmt.Errorf("did not find zone with domain %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &zones.Zones[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.stackpath.com/en/api/dns/#operation/GetZoneRecords
|
||||||
|
func (d *DNSProvider) getZoneRecords(name string, zone *Zone) ([]Record, error) {
|
||||||
|
u := fmt.Sprintf("/zones/%s/records", zone.ID)
|
||||||
|
req, err := d.newRequest(http.MethodGet, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Add("page_request.filter", fmt.Sprintf("name='%s' and type='TXT'", name))
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
var records Records
|
||||||
|
err = d.do(req, &records)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(records.Records) == 0 {
|
||||||
|
return nil, fmt.Errorf("did not find record with name %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records.Records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.stackpath.com/en/api/dns/#operation/CreateZoneRecord
|
||||||
|
func (d *DNSProvider) createZoneRecord(zone *Zone, record Record) error {
|
||||||
|
u := fmt.Sprintf("/zones/%s/records", zone.ID)
|
||||||
|
req, err := d.newRequest(http.MethodPost, u, record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.do(req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.stackpath.com/en/api/dns/#operation/DeleteZoneRecord
|
||||||
|
func (d *DNSProvider) deleteZoneRecord(zone *Zone, record Record) error {
|
||||||
|
u := fmt.Sprintf("/zones/%s/records/%s", zone.ID, record.ID)
|
||||||
|
req, err := d.newRequest(http.MethodDelete, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.do(req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSProvider) newRequest(method, urlStr string, body interface{}) (*http.Request, error) {
|
||||||
|
u, err := d.BaseURL.Parse(path.Join(d.config.StackID, urlStr))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if body == nil {
|
||||||
|
var req *http.Request
|
||||||
|
req, err = http.NewRequest(method, u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSProvider) do(req *http.Request, v interface{}) error {
|
||||||
|
resp, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := readBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(raw, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unmarshaling error: %v: %s", err, string(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkResponse(resp *http.Response) error {
|
||||||
|
if resp.StatusCode > 299 {
|
||||||
|
data, err := readBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return &ErrorResponse{Code: resp.StatusCode, Message: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
errResp := &ErrorResponse{}
|
||||||
|
err = json.Unmarshal(data, errResp)
|
||||||
|
if err != nil {
|
||||||
|
return &ErrorResponse{Code: resp.StatusCode, Message: fmt.Sprintf("unmarshaling error: %v: %s", err, string(data))}
|
||||||
|
}
|
||||||
|
return errResp
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBody(resp *http.Response) ([]byte, error) {
|
||||||
|
if resp.Body == nil {
|
||||||
|
return nil, fmt.Errorf("response body is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
rawBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawBody, nil
|
||||||
|
}
|
150
providers/dns/stackpath/stackpath.go
Normal file
150
providers/dns/stackpath/stackpath.go
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
// Package stackpath implements a DNS provider for solving the DNS-01 challenge using Stackpath DNS.
|
||||||
|
// https://developer.stackpath.com/en/api/dns/
|
||||||
|
package stackpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
|
"github.com/xenolf/lego/platform/config/env"
|
||||||
|
"golang.org/x/oauth2/clientcredentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultBaseURL = "https://gateway.stackpath.com/dns/v1/stacks/"
|
||||||
|
defaultAuthURL = "https://gateway.stackpath.com/identity/v1/oauth2/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is used to configure the creation of the DNSProvider
|
||||||
|
type Config struct {
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
StackID string
|
||||||
|
TTL int
|
||||||
|
PropagationTimeout time.Duration
|
||||||
|
PollingInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a default configuration for the DNSProvider
|
||||||
|
func NewDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
TTL: env.GetOrDefaultInt("STACKPATH_TTL", 120),
|
||||||
|
PropagationTimeout: env.GetOrDefaultSecond("STACKPATH_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
|
||||||
|
PollingInterval: env.GetOrDefaultSecond("STACKPATH_POLLING_INTERVAL", acme.DefaultPollingInterval),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
|
||||||
|
type DNSProvider struct {
|
||||||
|
BaseURL *url.URL
|
||||||
|
client *http.Client
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance configured for Stackpath.
|
||||||
|
// Credentials must be passed in the environment variables:
|
||||||
|
// STACKPATH_CLIENT_ID, STACKPATH_CLIENT_SECRET, and STACKPATH_STACK_ID.
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
values, err := env.Get("STACKPATH_CLIENT_ID", "STACKPATH_CLIENT_SECRET", "STACKPATH_STACK_ID")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stackpath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.ClientID = values["STACKPATH_CLIENT_ID"]
|
||||||
|
config.ClientSecret = values["STACKPATH_CLIENT_SECRET"]
|
||||||
|
config.StackID = values["STACKPATH_STACK_ID"]
|
||||||
|
|
||||||
|
return NewDNSProviderConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderConfig return a DNSProvider instance configured for Stackpath.
|
||||||
|
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("stackpath: the configuration of the DNS provider is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.ClientID) == 0 || len(config.ClientSecret) == 0 {
|
||||||
|
return nil, errors.New("stackpath: credentials missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.StackID) == 0 {
|
||||||
|
return nil, errors.New("stackpath: stack id missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL, _ := url.Parse(defaultBaseURL)
|
||||||
|
|
||||||
|
return &DNSProvider{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
client: getOathClient(config),
|
||||||
|
config: config,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOathClient(config *Config) *http.Client {
|
||||||
|
oathConfig := &clientcredentials.Config{
|
||||||
|
TokenURL: defaultAuthURL,
|
||||||
|
ClientID: config.ClientID,
|
||||||
|
ClientSecret: config.ClientSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
return oathConfig.Client(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfill the dns-01 challenge
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
zone, err := d.getZones(domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stackpath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
parts := strings.Split(fqdn, ".")
|
||||||
|
|
||||||
|
record := Record{
|
||||||
|
Name: parts[0],
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: d.config.TTL,
|
||||||
|
Data: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.createZoneRecord(zone, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
zone, err := d.getZones(domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stackpath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
parts := strings.Split(fqdn, ".")
|
||||||
|
|
||||||
|
records, err := d.getZoneRecords(parts[0], zone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
err = d.deleteZoneRecord(zone, record)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("stackpath: failed to delete TXT 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
|
||||||
|
}
|
315
providers/dns/stackpath/stackpath_test.go
Normal file
315
providers/dns/stackpath/stackpath_test.go
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
package stackpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
stackpathLiveTest bool
|
||||||
|
stackpathClientID string
|
||||||
|
stackpathClientSecret string
|
||||||
|
stackpathStackID string
|
||||||
|
stackpathDomain string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
stackpathClientID = os.Getenv("STACKPATH_CLIENT_ID")
|
||||||
|
stackpathClientSecret = os.Getenv("STACKPATH_CLIENT_SECRET")
|
||||||
|
stackpathStackID = os.Getenv("STACKPATH_STACK_ID")
|
||||||
|
stackpathDomain = os.Getenv("STACKPATH_DOMAIN")
|
||||||
|
|
||||||
|
if len(stackpathClientID) > 0 &&
|
||||||
|
len(stackpathClientSecret) > 0 &&
|
||||||
|
len(stackpathStackID) > 0 &&
|
||||||
|
len(stackpathDomain) > 0 {
|
||||||
|
stackpathLiveTest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreEnv() {
|
||||||
|
os.Setenv("STACKPATH_CLIENT_ID", stackpathClientID)
|
||||||
|
os.Setenv("STACKPATH_CLIENT_SECRET", stackpathClientSecret)
|
||||||
|
os.Setenv("STACKPATH_STACK_ID", stackpathStackID)
|
||||||
|
os.Setenv("STACKPATH_DOMAIN", stackpathDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLivePresent(t *testing.T) {
|
||||||
|
if !stackpathLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.Present(stackpathDomain, "", "123d==")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveCleanUp(t *testing.T) {
|
||||||
|
if !stackpathLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
|
provider, err := NewDNSProvider()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = provider.CleanUp(stackpathDomain, "", "123d==")
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProvider(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
envVars map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"STACKPATH_CLIENT_ID": "test@example.com",
|
||||||
|
"STACKPATH_CLIENT_SECRET": "123",
|
||||||
|
"STACKPATH_STACK_ID": "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing credentials",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"STACKPATH_CLIENT_ID": "",
|
||||||
|
"STACKPATH_CLIENT_SECRET": "",
|
||||||
|
"STACKPATH_STACK_ID": "",
|
||||||
|
},
|
||||||
|
expected: "stackpath: some credentials information are missing: STACKPATH_CLIENT_ID,STACKPATH_CLIENT_SECRET,STACKPATH_STACK_ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing client id",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"STACKPATH_CLIENT_ID": "",
|
||||||
|
"STACKPATH_CLIENT_SECRET": "123",
|
||||||
|
"STACKPATH_STACK_ID": "ID",
|
||||||
|
},
|
||||||
|
expected: "stackpath: some credentials information are missing: STACKPATH_CLIENT_ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing client secret",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"STACKPATH_CLIENT_ID": "test@example.com",
|
||||||
|
"STACKPATH_CLIENT_SECRET": "",
|
||||||
|
"STACKPATH_STACK_ID": "ID",
|
||||||
|
},
|
||||||
|
expected: "stackpath: some credentials information are missing: STACKPATH_CLIENT_SECRET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing stack id",
|
||||||
|
envVars: map[string]string{
|
||||||
|
"STACKPATH_CLIENT_ID": "test@example.com",
|
||||||
|
"STACKPATH_CLIENT_SECRET": "123",
|
||||||
|
"STACKPATH_STACK_ID": "",
|
||||||
|
},
|
||||||
|
expected: "stackpath: some credentials information are missing: STACKPATH_STACK_ID",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
defer restoreEnv()
|
||||||
|
for key, value := range test.envVars {
|
||||||
|
if len(value) == 0 {
|
||||||
|
os.Unsetenv(key)
|
||||||
|
} else {
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewDNSProvider()
|
||||||
|
|
||||||
|
if len(test.expected) == 0 {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
config *Config
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
"no_config": {
|
||||||
|
config: nil,
|
||||||
|
expectedErr: "stackpath: the configuration of the DNS provider is nil",
|
||||||
|
},
|
||||||
|
"no_client_id": {
|
||||||
|
config: &Config{
|
||||||
|
ClientSecret: "secret",
|
||||||
|
StackID: "stackID",
|
||||||
|
},
|
||||||
|
expectedErr: "stackpath: credentials missing",
|
||||||
|
},
|
||||||
|
"no_client_secret": {
|
||||||
|
config: &Config{
|
||||||
|
ClientID: "clientID",
|
||||||
|
StackID: "stackID",
|
||||||
|
},
|
||||||
|
expectedErr: "stackpath: credentials missing",
|
||||||
|
},
|
||||||
|
"no_stack_id": {
|
||||||
|
config: &Config{
|
||||||
|
ClientID: "clientID",
|
||||||
|
ClientSecret: "secret",
|
||||||
|
},
|
||||||
|
expectedErr: "stackpath: stack id missing",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for desc, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p, err := NewDNSProviderConfig(test.config)
|
||||||
|
require.EqualError(t, err, test.expectedErr)
|
||||||
|
assert.Nil(t, p)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMockAPITest() (*DNSProvider, *http.ServeMux, func()) {
|
||||||
|
apiHandler := http.NewServeMux()
|
||||||
|
server := httptest.NewServer(apiHandler)
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.ClientID = "CLIENT_ID"
|
||||||
|
config.ClientSecret = "CLIENT_SECRET"
|
||||||
|
config.StackID = "STACK_ID"
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.client = http.DefaultClient
|
||||||
|
provider.BaseURL, _ = url.Parse(server.URL + "/")
|
||||||
|
|
||||||
|
return provider, apiHandler, server.Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSProvider_getZoneRecords(t *testing.T) {
|
||||||
|
provider, mux, tearDown := setupMockAPITest()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
content := `
|
||||||
|
{
|
||||||
|
"records": [
|
||||||
|
{"id":"1","name":"foo1","type":"TXT","ttl":120,"data":"txtTXTtxt"},
|
||||||
|
{"id":"2","name":"foo2","type":"TXT","ttl":121,"data":"TXTtxtTXT"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
_, err := w.Write([]byte(content))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
records, err := provider.getZoneRecords("foo1", &Zone{ID: "A", Domain: "test"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []Record{
|
||||||
|
{ID: "1", Name: "foo1", Type: "TXT", TTL: 120, Data: "txtTXTtxt"},
|
||||||
|
{ID: "2", Name: "foo2", Type: "TXT", TTL: 121, Data: "TXTtxtTXT"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSProvider_getZoneRecords_apiError(t *testing.T) {
|
||||||
|
provider, mux, tearDown := setupMockAPITest()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
content := `
|
||||||
|
{
|
||||||
|
"code": 401,
|
||||||
|
"error": "an unauthorized request is attempted."
|
||||||
|
}`
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
_, err := w.Write([]byte(content))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := provider.getZoneRecords("foo1", &Zone{ID: "A", Domain: "test"})
|
||||||
|
|
||||||
|
expected := &ErrorResponse{Code: 401, Message: "an unauthorized request is attempted."}
|
||||||
|
assert.Equal(t, expected, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNSProvider_getZones(t *testing.T) {
|
||||||
|
provider, mux, tearDown := setupMockAPITest()
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
mux.HandleFunc("/STACK_ID/zones", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
content := `
|
||||||
|
{
|
||||||
|
"pageInfo": {
|
||||||
|
"totalCount": "5",
|
||||||
|
"hasPreviousPage": false,
|
||||||
|
"hasNextPage": false,
|
||||||
|
"startCursor": "1",
|
||||||
|
"endCursor": "1"
|
||||||
|
},
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"stackId": "my_stack",
|
||||||
|
"accountId": "my_account",
|
||||||
|
"id": "A",
|
||||||
|
"domain": "foo.com",
|
||||||
|
"version": "1",
|
||||||
|
"labels": {
|
||||||
|
"property1": "val1",
|
||||||
|
"property2": "val2"
|
||||||
|
},
|
||||||
|
"created": "2018-10-07T02:31:49Z",
|
||||||
|
"updated": "2018-10-07T02:31:49Z",
|
||||||
|
"nameservers": [
|
||||||
|
"1.1.1.1"
|
||||||
|
],
|
||||||
|
"verified": "2018-10-07T02:31:49Z",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
_, err := w.Write([]byte(content))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
zone, err := provider.getZones("sub.foo.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &Zone{ID: "A", Domain: "foo.com"}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, zone)
|
||||||
|
}
|
109
vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go
generated
vendored
Normal file
109
vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go
generated
vendored
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package clientcredentials implements the OAuth2.0 "client credentials" token flow,
|
||||||
|
// also known as the "two-legged OAuth 2.0".
|
||||||
|
//
|
||||||
|
// This should be used when the client is acting on its own behalf or when the client
|
||||||
|
// is the resource owner. It may also be used when requesting access to protected
|
||||||
|
// resources based on an authorization previously arranged with the authorization
|
||||||
|
// server.
|
||||||
|
//
|
||||||
|
// See https://tools.ietf.org/html/rfc6749#section-4.4
|
||||||
|
package clientcredentials // import "golang.org/x/oauth2/clientcredentials"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config describes a 2-legged OAuth2 flow, with both the
|
||||||
|
// client application information and the server's endpoint URLs.
|
||||||
|
type Config struct {
|
||||||
|
// ClientID is the application's ID.
|
||||||
|
ClientID string
|
||||||
|
|
||||||
|
// ClientSecret is the application's secret.
|
||||||
|
ClientSecret string
|
||||||
|
|
||||||
|
// TokenURL is the resource server's token endpoint
|
||||||
|
// URL. This is a constant specific to each server.
|
||||||
|
TokenURL string
|
||||||
|
|
||||||
|
// Scope specifies optional requested permissions.
|
||||||
|
Scopes []string
|
||||||
|
|
||||||
|
// EndpointParams specifies additional parameters for requests to the token endpoint.
|
||||||
|
EndpointParams url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token uses client credentials to retrieve a token.
|
||||||
|
// The HTTP client to use is derived from the context.
|
||||||
|
// If nil, http.DefaultClient is used.
|
||||||
|
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
|
||||||
|
return c.TokenSource(ctx).Token()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns an HTTP client using the provided token.
|
||||||
|
// The token will auto-refresh as necessary. The underlying
|
||||||
|
// HTTP transport will be obtained using the provided context.
|
||||||
|
// The returned client and its Transport should not be modified.
|
||||||
|
func (c *Config) Client(ctx context.Context) *http.Client {
|
||||||
|
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenSource returns a TokenSource that returns t until t expires,
|
||||||
|
// automatically refreshing it as necessary using the provided context and the
|
||||||
|
// client ID and client secret.
|
||||||
|
//
|
||||||
|
// Most users will use Config.Client instead.
|
||||||
|
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||||
|
source := &tokenSource{
|
||||||
|
ctx: ctx,
|
||||||
|
conf: c,
|
||||||
|
}
|
||||||
|
return oauth2.ReuseTokenSource(nil, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenSource struct {
|
||||||
|
ctx context.Context
|
||||||
|
conf *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token refreshes the token by using a new client credentials request.
|
||||||
|
// tokens received this way do not include a refresh token
|
||||||
|
func (c *tokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
v := url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
}
|
||||||
|
if len(c.conf.Scopes) > 0 {
|
||||||
|
v.Set("scope", strings.Join(c.conf.Scopes, " "))
|
||||||
|
}
|
||||||
|
for k, p := range c.conf.EndpointParams {
|
||||||
|
if _, ok := v[k]; ok {
|
||||||
|
return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k)
|
||||||
|
}
|
||||||
|
v[k] = p
|
||||||
|
}
|
||||||
|
tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v)
|
||||||
|
if err != nil {
|
||||||
|
if rErr, ok := err.(*internal.RetrieveError); ok {
|
||||||
|
return nil, (*oauth2.RetrieveError)(rErr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := &oauth2.Token{
|
||||||
|
AccessToken: tk.AccessToken,
|
||||||
|
TokenType: tk.TokenType,
|
||||||
|
RefreshToken: tk.RefreshToken,
|
||||||
|
Expiry: tk.Expiry,
|
||||||
|
}
|
||||||
|
return t.WithExtra(tk.Raw), nil
|
||||||
|
}
|
Loading…
Reference in a new issue