From 7eeac6313980bd4b4a1a1a324bf80e8c7dc7d309 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 11 Oct 2018 16:50:04 +0200 Subject: [PATCH] Fix: acme DNS providers --- Gopkg.lock | 4 +- docs/configuration/acme.md | 1 + .../lego/providers/dns/bluecat/bluecat.go | 2 +- .../lego/providers/dns/cloudxns/cloudxns.go | 2 +- .../lego/providers/dns/dns_providers.go | 3 + .../lego/providers/dns/dnspod/dnspod.go | 2 +- .../lego/providers/dns/glesys/glesys.go | 1 + .../providers/dns/sakuracloud/sakuracloud.go | 2 +- .../lego/providers/dns/stackpath/client.go | 217 ++++++++++++++++++ .../lego/providers/dns/stackpath/stackpath.go | 150 ++++++++++++ .../clientcredentials/clientcredentials.go | 109 +++++++++ 11 files changed, 488 insertions(+), 5 deletions(-) create mode 100644 vendor/github.com/xenolf/lego/providers/dns/stackpath/client.go create mode 100644 vendor/github.com/xenolf/lego/providers/dns/stackpath/stackpath.go create mode 100644 vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go diff --git a/Gopkg.lock b/Gopkg.lock index b7af1c85f..ba40ade13 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1393,10 +1393,11 @@ "providers/dns/rfc2136", "providers/dns/route53", "providers/dns/sakuracloud", + "providers/dns/stackpath", "providers/dns/vegadns", "providers/dns/vultr" ] - revision = "dd087560a0a4a52b3388dd320a5982a0e8233eff" + revision = "01c63ec08d1d85e3ad44c16dff95dadee26a81bc" [[projects]] branch = "master" @@ -1442,6 +1443,7 @@ name = "golang.org/x/oauth2" packages = [ ".", + "clientcredentials", "google", "internal", "jws", diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index d147109d6..c49664142 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -291,6 +291,7 @@ Here is a list of supported `provider`s, that can automate the DNS verification, | [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet | | [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `[AWS_REGION]`, `[AWS_HOSTED_ZONE_ID]` or a configured user/instance IAM profile. | YES | | [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet | +| [Stackpath](https://www.stackpath.com/) | `stackpath` | `STACKPATH_CLIENT_ID`, `STACKPATH_CLIENT_SECRET`, `STACKPATH_STACK_ID` | Not tested yet | | [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | | [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | diff --git a/vendor/github.com/xenolf/lego/providers/dns/bluecat/bluecat.go b/vendor/github.com/xenolf/lego/providers/dns/bluecat/bluecat.go index 30484bc26..fabfdae3b 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/bluecat/bluecat.go +++ b/vendor/github.com/xenolf/lego/providers/dns/bluecat/bluecat.go @@ -61,7 +61,7 @@ type DNSProvider struct { // The REST endpoint will be appended. In addition, the Configuration name // and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and BLUECAT_DNS_VIEW func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get("BLUECAT_SERVER_URL", "BLUECAT_USER_NAME", "BLUECAT_CONFIG_NAME", "BLUECAT_CONFIG_NAME", "BLUECAT_DNS_VIEW") + values, err := env.Get("BLUECAT_SERVER_URL", "BLUECAT_USER_NAME", "BLUECAT_PASSWORD", "BLUECAT_CONFIG_NAME", "BLUECAT_DNS_VIEW") if err != nil { return nil, fmt.Errorf("bluecat: %v", err) } diff --git a/vendor/github.com/xenolf/lego/providers/dns/cloudxns/cloudxns.go b/vendor/github.com/xenolf/lego/providers/dns/cloudxns/cloudxns.go index 106db817c..6f4bb6bd7 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/cloudxns/cloudxns.go +++ b/vendor/github.com/xenolf/lego/providers/dns/cloudxns/cloudxns.go @@ -76,7 +76,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client.HTTPClient = config.HTTPClient - return &DNSProvider{client: client}, nil + return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. diff --git a/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go b/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go index a3a5f1ef9..377346631 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go +++ b/vendor/github.com/xenolf/lego/providers/dns/dns_providers.go @@ -42,6 +42,7 @@ import ( "github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/route53" "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/vultr" ) @@ -127,6 +128,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return rfc2136.NewDNSProvider() case "sakuracloud": return sakuracloud.NewDNSProvider() + case "stackpath": + return stackpath.NewDNSProvider() case "vegadns": return vegadns.NewDNSProvider() case "vultr": diff --git a/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go b/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go index c5c27c1e1..1aa8d9e97 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go +++ b/vendor/github.com/xenolf/lego/providers/dns/dnspod/dnspod.go @@ -81,7 +81,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client := dnspod.NewClient(params) client.HttpClient = config.HTTPClient - return &DNSProvider{client: client}, nil + return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. diff --git a/vendor/github.com/xenolf/lego/providers/dns/glesys/glesys.go b/vendor/github.com/xenolf/lego/providers/dns/glesys/glesys.go index 1354d1918..69f61f772 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/glesys/glesys.go +++ b/vendor/github.com/xenolf/lego/providers/dns/glesys/glesys.go @@ -94,6 +94,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } return &DNSProvider{ + config: config, activeRecords: make(map[string]int), }, nil } diff --git a/vendor/github.com/xenolf/lego/providers/dns/sakuracloud/sakuracloud.go b/vendor/github.com/xenolf/lego/providers/dns/sakuracloud/sakuracloud.go index b8718d0e1..b0227f6ce 100644 --- a/vendor/github.com/xenolf/lego/providers/dns/sakuracloud/sakuracloud.go +++ b/vendor/github.com/xenolf/lego/providers/dns/sakuracloud/sakuracloud.go @@ -82,7 +82,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { client := api.NewClient(config.Token, config.Secret, "tk1a") client.UserAgent = acme.UserAgent - return &DNSProvider{client: client}, nil + return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. diff --git a/vendor/github.com/xenolf/lego/providers/dns/stackpath/client.go b/vendor/github.com/xenolf/lego/providers/dns/stackpath/client.go new file mode 100644 index 000000000..495d8c555 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/stackpath/client.go @@ -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 +} diff --git a/vendor/github.com/xenolf/lego/providers/dns/stackpath/stackpath.go b/vendor/github.com/xenolf/lego/providers/dns/stackpath/stackpath.go new file mode 100644 index 000000000..4c247def7 --- /dev/null +++ b/vendor/github.com/xenolf/lego/providers/dns/stackpath/stackpath.go @@ -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 +} diff --git a/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go new file mode 100644 index 000000000..c4e840d22 --- /dev/null +++ b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go @@ -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 +}