Update Lego

This commit is contained in:
Ludovic Fernandez 2018-09-14 10:06:03 +02:00 committed by Traefiker Bot
parent 36966da701
commit 253060b4f3
185 changed files with 16653 additions and 3210 deletions

View file

@ -41,6 +41,17 @@ type solver interface {
Solve(challenge challenge, domain string) error
}
// Interface for challenges like dns, where we can set a record in advance for ALL challenges.
// This saves quite a bit of time vs creating the records and solving them serially.
type presolver interface {
PreSolve(challenge challenge, domain string) error
}
// Interface for challenges like dns, where we can solve all the challenges before to delete them.
type cleanup interface {
CleanUp(challenge challenge, domain string) error
}
type validateFunc func(j *jws, domain, uri string, chlng challenge) error
// Client is the user-friendy way to ACME
@ -374,8 +385,10 @@ DNSNames:
}
}
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = pemEncode(&csr)
if cert != nil {
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = pemEncode(&csr)
}
// do not return an empty failures map, because
// it would still be a non-nil error value
@ -548,29 +561,75 @@ func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, err
return orderRes, nil
}
// an authz with the solver we have chosen and the index of the challenge associated with it
type selectedAuthSolver struct {
authz authorization
challengeIndex int
solver solver
}
// Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns.
func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
failures := make(ObtainError)
// loop through the resources, basically through the domains.
authSolvers := []*selectedAuthSolver{}
// loop through the resources, basically through the domains. First pass just selects a solver for each authz.
for _, authz := range authorizations {
if authz.Status == "valid" {
// Boulder might recycle recent validated authz (see issue #267)
log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
continue
}
// no solvers - no solving
if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil {
err := solver.Solve(authz.Challenges[i], authz.Identifier.Value)
if err != nil {
//c.disableAuthz(authz.Identifier)
authSolvers = append(authSolvers, &selectedAuthSolver{
authz: authz,
challengeIndex: i,
solver: solver,
})
} else {
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
}
}
// for all valid presolvers, first submit the challenges so they have max time to propigate
for _, item := range authSolvers {
authz := item.authz
i := item.challengeIndex
if presolver, ok := item.solver.(presolver); ok {
if err := presolver.PreSolve(authz.Challenges[i], authz.Identifier.Value); err != nil {
failures[authz.Identifier.Value] = err
}
} else {
//c.disableAuthz(authz)
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
}
}
defer func() {
// clean all created TXT records
for _, item := range authSolvers {
if cleanup, ok := item.solver.(cleanup); ok {
if failures[item.authz.Identifier.Value] != nil {
// already failed in previous loop
continue
}
err := cleanup.CleanUp(item.authz.Challenges[item.challengeIndex], item.authz.Identifier.Value)
if err != nil {
log.Warnf("Error cleaning up %s: %v ", item.authz.Identifier.Value, err)
}
}
}
}()
// finally solve all challenges for real
for _, item := range authSolvers {
authz := item.authz
i := item.challengeIndex
if failures[authz.Identifier.Value] != nil {
// already failed in previous loop
continue
}
if err := item.solver.Solve(authz.Challenges[i], authz.Identifier.Value); err != nil {
failures[authz.Identifier.Value] = err
}
}

View file

@ -71,8 +71,10 @@ type dnsChallenge struct {
provider ChallengeProvider
}
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
// PreSolve just submits the txt record to the dns provider. It does not validate record propagation, or
// do anything at all with the acme server.
func (s *dnsChallenge) PreSolve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
if s.provider == nil {
return errors.New("no DNS Provider configured")
@ -88,12 +90,18 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
if err != nil {
return fmt.Errorf("error presenting token: %s", err)
}
defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Warnf("Error cleaning up %s: %v ", domain, err)
}
}()
return nil
}
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
fqdn, value, _ := DNS01Record(domain, keyAuth)
@ -117,6 +125,15 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
// CleanUp cleans the challenge
func (s *dnsChallenge) CleanUp(chlng challenge, domain string) error {
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
return s.provider.CleanUp(domain, chlng.Token, keyAuth)
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string) (bool, error) {
// Initial attempt to resolve at the recursive NS

View file

@ -3,6 +3,7 @@ package env
import (
"fmt"
"os"
"strconv"
"strings"
)
@ -25,3 +26,14 @@ func Get(names ...string) (map[string]string, error) {
return values, nil
}
// GetOrDefaultInt returns the given environment variable value as an integer.
// Returns the default if the envvar cannot be coopered to an int, or is not found.
func GetOrDefaultInt(envVar string, defaultValue int) int {
v, err := strconv.Atoi(os.Getenv(envVar))
if err != nil {
return defaultValue
}
return v
}

View file

@ -0,0 +1,166 @@
// Package alidns implements a DNS provider for solving the DNS-01 challenge
// using Alibaba Cloud DNS.
package alidns
import (
"fmt"
"os"
"strings"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
const defaultRegionID = "cn-hangzhou"
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
client *alidns.Client
}
// NewDNSProvider returns a DNSProvider instance configured for Alibaba Cloud DNS.
// Credentials must be passed in the environment variables: ALICLOUD_ACCESS_KEY and ALICLOUD_SECRET_KEY.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("ALICLOUD_ACCESS_KEY", "ALICLOUD_SECRET_KEY")
if err != nil {
return nil, fmt.Errorf("AliDNS: %v", err)
}
regionID := os.Getenv("ALICLOUD_REGION_ID")
return NewDNSProviderCredentials(values["ALICLOUD_ACCESS_KEY"], values["ALICLOUD_SECRET_KEY"], regionID)
}
// NewDNSProviderCredentials uses the supplied credentials to return a DNSProvider instance configured for alidns.
func NewDNSProviderCredentials(apiKey, secretKey, regionID string) (*DNSProvider, error) {
if apiKey == "" || secretKey == "" {
return nil, fmt.Errorf("AliDNS: credentials missing")
}
if len(regionID) == 0 {
regionID = defaultRegionID
}
client, err := alidns.NewClientWithAccessKey(regionID, apiKey, secretKey)
if err != nil {
return nil, fmt.Errorf("AliDNS: credentials failed: %v", err)
}
return &DNSProvider{
client: client,
}, nil
}
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
_, zoneName, err := d.getHostedZone(domain)
if err != nil {
return err
}
recordAttributes := d.newTxtRecord(zoneName, fqdn, value, ttl)
_, err = d.client.AddDomainRecord(recordAttributes)
if err != nil {
return fmt.Errorf("AliDNS: API call failed: %v", err)
}
return nil
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
records, err := d.findTxtRecords(domain, fqdn)
if err != nil {
return err
}
_, _, err = d.getHostedZone(domain)
if err != nil {
return err
}
for _, rec := range records {
request := alidns.CreateDeleteDomainRecordRequest()
request.RecordId = rec.RecordId
_, err = d.client.DeleteDomainRecord(request)
if err != nil {
return err
}
}
return nil
}
func (d *DNSProvider) getHostedZone(domain string) (string, string, error) {
request := alidns.CreateDescribeDomainsRequest()
zones, err := d.client.DescribeDomains(request)
if err != nil {
return "", "", fmt.Errorf("AliDNS: API call failed: %v", err)
}
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return "", "", err
}
var hostedZone alidns.Domain
for _, zone := range zones.Domains.Domain {
if zone.DomainName == acme.UnFqdn(authZone) {
hostedZone = zone
}
}
if hostedZone.DomainId == "" {
return "", "", fmt.Errorf("AliDNS: zone %s not found in AliDNS for domain %s", authZone, domain)
}
return fmt.Sprintf("%v", hostedZone.DomainId), hostedZone.DomainName, nil
}
func (d *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *alidns.AddDomainRecordRequest {
request := alidns.CreateAddDomainRecordRequest()
request.Type = "TXT"
request.DomainName = zone
request.RR = d.extractRecordName(fqdn, zone)
request.Value = value
request.TTL = requests.NewInteger(600)
return request
}
func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]alidns.Record, error) {
_, zoneName, err := d.getHostedZone(domain)
if err != nil {
return nil, err
}
request := alidns.CreateDescribeDomainRecordsRequest()
request.DomainName = zoneName
request.PageSize = requests.NewInteger(500)
var records []alidns.Record
result, err := d.client.DescribeDomainRecords(request)
if err != nil {
return records, fmt.Errorf("AliDNS: API call has failed: %v", err)
}
recordName := d.extractRecordName(fqdn, zoneName)
for _, record := range result.DomainRecords.Record {
if record.RR == recordName {
records = append(records, record)
}
}
return records, nil
}
func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
name := acme.UnFqdn(fqdn)
if idx := strings.Index(name, "."+domain); idx != -1 {
return name[:idx]
}
return name
}

View file

@ -5,6 +5,7 @@ import (
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/providers/dns/acmedns"
"github.com/xenolf/lego/providers/dns/alidns"
"github.com/xenolf/lego/providers/dns/auroradns"
"github.com/xenolf/lego/providers/dns/azure"
"github.com/xenolf/lego/providers/dns/bluecat"
@ -24,10 +25,12 @@ import (
"github.com/xenolf/lego/providers/dns/gcloud"
"github.com/xenolf/lego/providers/dns/glesys"
"github.com/xenolf/lego/providers/dns/godaddy"
"github.com/xenolf/lego/providers/dns/iij"
"github.com/xenolf/lego/providers/dns/lightsail"
"github.com/xenolf/lego/providers/dns/linode"
"github.com/xenolf/lego/providers/dns/namecheap"
"github.com/xenolf/lego/providers/dns/namedotcom"
"github.com/xenolf/lego/providers/dns/netcup"
"github.com/xenolf/lego/providers/dns/nifcloud"
"github.com/xenolf/lego/providers/dns/ns1"
"github.com/xenolf/lego/providers/dns/otc"
@ -46,6 +49,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
switch name {
case "acme-dns":
return acmedns.NewDNSProvider()
case "alidns":
return alidns.NewDNSProvider()
case "azure":
return azure.NewDNSProvider()
case "auroradns":
@ -82,6 +87,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
return gcloud.NewDNSProvider()
case "godaddy":
return godaddy.NewDNSProvider()
case "iij":
return iij.NewDNSProvider()
case "lightsail":
return lightsail.NewDNSProvider()
case "linode":
@ -92,6 +99,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
return namecheap.NewDNSProvider()
case "namedotcom":
return namedotcom.NewDNSProvider()
case "netcup":
return netcup.NewDNSProvider()
case "nifcloud":
return nifcloud.NewDNSProvider()
case "rackspace":

View file

@ -58,20 +58,27 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return err
}
record := egoscale.DNSRecord{
Name: recordName,
TTL: ttl,
Content: value,
RecordType: "TXT",
}
if recordID == 0 {
record := egoscale.DNSRecord{
Name: recordName,
TTL: ttl,
Content: value,
RecordType: "TXT",
}
_, err := d.client.CreateRecord(zone, record)
if err != nil {
return errors.New("Error while creating DNS record: " + err.Error())
}
} else {
record.ID = recordID
record := egoscale.UpdateDNSRecord{
ID: recordID,
Name: recordName,
TTL: ttl,
Content: value,
RecordType: "TXT",
}
_, err := d.client.UpdateRecord(zone, record)
if err != nil {
return errors.New("Error while updating DNS record: " + err.Error())

211
vendor/github.com/xenolf/lego/providers/dns/iij/iij.go generated vendored Normal file
View file

@ -0,0 +1,211 @@
// Package iij implements a DNS provider for solving the DNS-01 challenge using IIJ DNS.
package iij
import (
"fmt"
"strings"
"time"
"github.com/iij/doapi"
"github.com/iij/doapi/protocol"
"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 {
AccessKey string
SecretKey string
DoServiceCode string
}
// DNSProvider implements the acme.ChallengeProvider interface
type DNSProvider struct {
api *doapi.API
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for IIJ DO
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("IIJ_API_ACCESS_KEY", "IIJ_API_SECRET_KEY", "IIJ_DO_SERVICE_CODE")
if err != nil {
return nil, fmt.Errorf("IIJ: %v", err)
}
return NewDNSProviderConfig(&Config{
AccessKey: values["IIJ_API_ACCESS_KEY"],
SecretKey: values["IIJ_API_SECRET_KEY"],
DoServiceCode: values["IIJ_DO_SERVICE_CODE"],
})
}
// NewDNSProviderConfig takes a given config ans returns a custom configured
// DNSProvider instance
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{
api: doapi.NewAPI(config.AccessKey, config.SecretKey),
config: config,
}, nil
}
// Timeout returns the timeout and interval to use when checking for DNS propagation.
func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
return time.Minute * 2, time.Second * 4
}
// Present creates a TXT record using the specified parameters
func (p *DNSProvider) Present(domain, token, keyAuth string) error {
_, value, _ := acme.DNS01Record(domain, keyAuth)
return p.addTxtRecord(domain, value)
}
// CleanUp removes the TXT record matching the specified parameters
func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
_, value, _ := acme.DNS01Record(domain, keyAuth)
return p.deleteTxtRecord(domain, value)
}
func (p *DNSProvider) addTxtRecord(domain, value string) error {
zones, err := p.listZones()
if err != nil {
return err
}
owner, zone, err := splitDomain(domain, zones)
if err != nil {
return err
}
request := protocol.RecordAdd{
DoServiceCode: p.config.DoServiceCode,
ZoneName: zone,
Owner: owner,
TTL: "300",
RecordType: "TXT",
RData: value,
}
response := &protocol.RecordAddResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
return err
}
return p.commit()
}
func (p *DNSProvider) deleteTxtRecord(domain, value string) error {
zones, err := p.listZones()
if err != nil {
return err
}
owner, zone, err := splitDomain(domain, zones)
if err != nil {
return err
}
id, err := p.findTxtRecord(owner, zone, value)
if err != nil {
return err
}
request := protocol.RecordDelete{
DoServiceCode: p.config.DoServiceCode,
ZoneName: zone,
RecordID: id,
}
response := &protocol.RecordDeleteResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
return err
}
return p.commit()
}
func (p *DNSProvider) commit() error {
request := protocol.Commit{
DoServiceCode: p.config.DoServiceCode,
}
response := &protocol.CommitResponse{}
return doapi.Call(*p.api, request, response)
}
func (p *DNSProvider) findTxtRecord(owner, zone, value string) (string, error) {
request := protocol.RecordListGet{
DoServiceCode: p.config.DoServiceCode,
ZoneName: zone,
}
response := &protocol.RecordListGetResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
return "", err
}
var id string
for _, record := range response.RecordList {
if record.Owner == owner && record.RecordType == "TXT" && record.RData == "\""+value+"\"" {
id = record.Id
}
}
if id == "" {
return "", fmt.Errorf("%s record in %s not found", owner, zone)
}
return id, nil
}
func (p *DNSProvider) listZones() ([]string, error) {
request := protocol.ZoneListGet{
DoServiceCode: p.config.DoServiceCode,
}
response := &protocol.ZoneListGetResponse{}
if err := doapi.Call(*p.api, request, response); err != nil {
return nil, err
}
return response.ZoneList, nil
}
func splitDomain(domain string, zones []string) (string, string, error) {
parts := strings.Split(strings.Trim(domain, "."), ".")
var owner string
var zone string
for i := 0; i < len(parts)-1; i++ {
zone = strings.Join(parts[i:], ".")
if zoneContains(zone, zones) {
baseOwner := strings.Join(parts[0:i], ".")
if len(baseOwner) > 0 {
baseOwner = "." + baseOwner
}
owner = "_acme-challenge" + baseOwner
break
}
}
if len(owner) == 0 {
return "", "", fmt.Errorf("%s not found", domain)
}
return owner, zone, nil
}
func zoneContains(zone string, zones []string) bool {
for _, z := range zones {
if zone == z {
return true
}
}
return false
}

View file

@ -0,0 +1,327 @@
package netcup
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/xenolf/lego/acme"
)
// netcupBaseURL for reaching the jSON-based API-Endpoint of netcup
const netcupBaseURL = "https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
// success response status
const success = "success"
// Request wrapper as specified in netcup wiki
// needed for every request to netcup API around *Msg
// https://www.netcup-wiki.de/wiki/CCP_API#Anmerkungen_zu_JSON-Requests
type Request struct {
Action string `json:"action"`
Param interface{} `json:"param"`
}
// LoginMsg as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#login
type LoginMsg struct {
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APIPassword string `json:"apipassword"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// LogoutMsg as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#logout
type LogoutMsg struct {
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APISessionID string `json:"apisessionid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// UpdateDNSRecordsMsg as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#updateDnsRecords
type UpdateDNSRecordsMsg struct {
DomainName string `json:"domainname"`
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APISessionID string `json:"apisessionid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
DNSRecordSet DNSRecordSet `json:"dnsrecordset"`
}
// DNSRecordSet as specified in netcup WSDL
// needed in UpdateDNSRecordsMsg
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecordset
type DNSRecordSet struct {
DNSRecords []DNSRecord `json:"dnsrecords"`
}
// InfoDNSRecordsMsg as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#infoDnsRecords
type InfoDNSRecordsMsg struct {
DomainName string `json:"domainname"`
CustomerNumber string `json:"customernumber"`
APIKey string `json:"apikey"`
APISessionID string `json:"apisessionid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
}
// DNSRecord as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Dnsrecord
type DNSRecord struct {
ID int `json:"id,string,omitempty"`
Hostname string `json:"hostname"`
RecordType string `json:"type"`
Priority string `json:"priority,omitempty"`
Destination string `json:"destination"`
DeleteRecord bool `json:"deleterecord,omitempty"`
State string `json:"state,omitempty"`
}
// ResponseMsg as specified in netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php#Responsemessage
type ResponseMsg struct {
ServerRequestID string `json:"serverrequestid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
Action string `json:"action"`
Status string `json:"status"`
StatusCode int `json:"statuscode"`
ShortMessage string `json:"shortmessage"`
LongMessage string `json:"longmessage"`
ResponseData ResponseData `json:"responsedata,omitempty"`
}
// LogoutResponseMsg similar to ResponseMsg
// allows empty ResponseData field whilst unmarshaling
type LogoutResponseMsg struct {
ServerRequestID string `json:"serverrequestid"`
ClientRequestID string `json:"clientrequestid,omitempty"`
Action string `json:"action"`
Status string `json:"status"`
StatusCode int `json:"statuscode"`
ShortMessage string `json:"shortmessage"`
LongMessage string `json:"longmessage"`
ResponseData string `json:"responsedata,omitempty"`
}
// ResponseData to enable correct unmarshaling of ResponseMsg
type ResponseData struct {
APISessionID string `json:"apisessionid"`
DNSRecords []DNSRecord `json:"dnsrecords"`
}
// Client netcup DNS client
type Client struct {
customerNumber string
apiKey string
apiPassword string
client *http.Client
}
// NewClient creates a netcup DNS client
func NewClient(httpClient *http.Client, customerNumber string, apiKey string, apiPassword string) *Client {
client := http.DefaultClient
if httpClient != nil {
client = httpClient
}
return &Client{
customerNumber: customerNumber,
apiKey: apiKey,
apiPassword: apiPassword,
client: client,
}
}
// Login performs the login as specified by the netcup WSDL
// returns sessionID needed to perform remaining actions
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) Login() (string, error) {
payload := &Request{
Action: "login",
Param: &LoginMsg{
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APIPassword: c.apiPassword,
ClientRequestID: "",
},
}
response, err := c.sendRequest(payload)
if err != nil {
return "", fmt.Errorf("netcup: error sending request to DNS-API, %v", err)
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return "", fmt.Errorf("netcup: error decoding response of DNS-API, %v", err)
}
if r.Status != success {
return "", fmt.Errorf("netcup: error logging into DNS-API, %v", r.LongMessage)
}
return r.ResponseData.APISessionID, nil
}
// Logout performs the logout with the supplied sessionID as specified by the netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) Logout(sessionID string) error {
payload := &Request{
Action: "logout",
Param: &LogoutMsg{
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: sessionID,
ClientRequestID: "",
},
}
response, err := c.sendRequest(payload)
if err != nil {
return fmt.Errorf("netcup: error logging out of DNS-API: %v", err)
}
var r LogoutResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return fmt.Errorf("netcup: error logging out of DNS-API: %v", err)
}
if r.Status != success {
return fmt.Errorf("netcup: error logging out of DNS-API: %v", r.ShortMessage)
}
return nil
}
// UpdateDNSRecord performs an update of the DNSRecords as specified by the netcup WSDL
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) UpdateDNSRecord(sessionID, domainName string, record DNSRecord) error {
payload := &Request{
Action: "updateDnsRecords",
Param: UpdateDNSRecordsMsg{
DomainName: domainName,
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: sessionID,
ClientRequestID: "",
DNSRecordSet: DNSRecordSet{DNSRecords: []DNSRecord{record}},
},
}
response, err := c.sendRequest(payload)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return fmt.Errorf("netcup: %v", err)
}
if r.Status != success {
return fmt.Errorf("netcup: %s: %+v", r.ShortMessage, r)
}
return nil
}
// GetDNSRecords retrieves all dns records of an DNS-Zone as specified by the netcup WSDL
// returns an array of DNSRecords
// https://ccp.netcup.net/run/webservice/servers/endpoint.php
func (c *Client) GetDNSRecords(hostname, apiSessionID string) ([]DNSRecord, error) {
payload := &Request{
Action: "infoDnsRecords",
Param: InfoDNSRecordsMsg{
DomainName: hostname,
CustomerNumber: c.customerNumber,
APIKey: c.apiKey,
APISessionID: apiSessionID,
ClientRequestID: "",
},
}
response, err := c.sendRequest(payload)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
var r ResponseMsg
err = json.Unmarshal(response, &r)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
if r.Status != success {
return nil, fmt.Errorf("netcup: %s", r.ShortMessage)
}
return r.ResponseData.DNSRecords, nil
}
// sendRequest marshals given body to JSON, send the request to netcup API
// and returns body of response
func (c *Client) sendRequest(payload interface{}) ([]byte, error) {
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
req, err := http.NewRequest(http.MethodPost, netcupBaseURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
req.Close = true
req.Header.Set("content-type", "application/json")
req.Header.Set("User-Agent", acme.UserAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
if resp.StatusCode > 299 {
return nil, fmt.Errorf("netcup: API request failed with HTTP Status code %d", resp.StatusCode)
}
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("netcup: read of response body failed, %v", err)
}
defer resp.Body.Close()
return body, nil
}
// GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord
// equivalence is determined by Destination and RecortType attributes
// returns index of given DNSRecord in given array of DNSRecords
func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) {
for index, element := range records {
if record.Destination == element.Destination && record.RecordType == element.RecordType {
return index, nil
}
}
return -1, fmt.Errorf("netcup: no DNS Record found")
}
// CreateTxtRecord uses the supplied values to return a DNSRecord of type TXT for the dns-01 challenge
func CreateTxtRecord(hostname, value string) DNSRecord {
return DNSRecord{
ID: 0,
Hostname: hostname,
RecordType: "TXT",
Priority: "",
Destination: value,
DeleteRecord: false,
State: "",
}
}

View file

@ -0,0 +1,116 @@
// Package netcup implements a DNS Provider for solving the DNS-01 challenge using the netcup DNS API.
package netcup
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
client *Client
}
// NewDNSProvider returns a DNSProvider instance configured for netcup.
// Credentials must be passed in the environment variables: NETCUP_CUSTOMER_NUMBER,
// NETCUP_API_KEY, NETCUP_API_PASSWORD
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get("NETCUP_CUSTOMER_NUMBER", "NETCUP_API_KEY", "NETCUP_API_PASSWORD")
if err != nil {
return nil, fmt.Errorf("netcup: %v", err)
}
return NewDNSProviderCredentials(values["NETCUP_CUSTOMER_NUMBER"], values["NETCUP_API_KEY"], values["NETCUP_API_PASSWORD"])
}
// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for netcup.
func NewDNSProviderCredentials(customer, key, password string) (*DNSProvider, error) {
if customer == "" || key == "" || password == "" {
return nil, fmt.Errorf("netcup: netcup credentials missing")
}
httpClient := &http.Client{
Timeout: 10 * time.Second,
}
return &DNSProvider{
client: NewClient(httpClient, customer, key, password),
}, nil
}
// Present creates a TXT record to fulfill the dns-01 challenge
func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domainName, keyAuth)
zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("netcup: failed to find DNSZone, %v", err)
}
sessionID, err := d.client.Login()
if err != nil {
return err
}
hostname := strings.Replace(fqdn, "."+zone, "", 1)
record := CreateTxtRecord(hostname, value)
err = d.client.UpdateDNSRecord(sessionID, acme.UnFqdn(zone), record)
if err != nil {
if errLogout := d.client.Logout(sessionID); errLogout != nil {
return fmt.Errorf("failed to add TXT-Record: %v; %v", err, errLogout)
}
return fmt.Errorf("failed to add TXT-Record: %v", err)
}
return d.client.Logout(sessionID)
}
// CleanUp removes the TXT record matching the specified parameters
func (d *DNSProvider) CleanUp(domainname, token, keyAuth string) error {
fqdn, value, _ := acme.DNS01Record(domainname, keyAuth)
zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("failed to find DNSZone, %v", err)
}
sessionID, err := d.client.Login()
if err != nil {
return err
}
hostname := strings.Replace(fqdn, "."+zone, "", 1)
zone = acme.UnFqdn(zone)
records, err := d.client.GetDNSRecords(zone, sessionID)
if err != nil {
return err
}
record := CreateTxtRecord(hostname, value)
idx, err := GetDNSRecordIdx(records, record)
if err != nil {
return err
}
records[idx].DeleteRecord = true
err = d.client.UpdateDNSRecord(sessionID, zone, records[idx])
if err != nil {
if errLogout := d.client.Logout(sessionID); errLogout != nil {
return fmt.Errorf("%v; %v", err, errLogout)
}
return err
}
return d.client.Logout(sessionID)
}

View file

@ -50,13 +50,17 @@ func NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, c
return nil, fmt.Errorf("OVH credentials missing")
}
ovhClient, _ := ovh.NewClient(
ovhClient, err := ovh.NewClient(
apiEndpoint,
applicationKey,
applicationSecret,
consumerKey,
)
if err != nil {
return nil, err
}
return &DNSProvider{
client: ovhClient,
recordIDs: make(map[string]int),
@ -65,31 +69,12 @@ func NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, c
// Present creates a TXT record to fulfil the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
// txtRecordRequest represents the request body to DO's API to make a TXT record
type txtRecordRequest struct {
FieldType string `json:"fieldType"`
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
}
// txtRecordResponse represents a response from DO's API after making a TXT record
type txtRecordResponse struct {
ID int `json:"id"`
FieldType string `json:"fieldType"`
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
Zone string `json:"zone"`
}
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
// Parse domain name
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
@ -133,7 +118,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
if err != nil {
return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err)
return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err)
}
authZone = acme.UnFqdn(authZone)
@ -160,3 +145,21 @@ func (d *DNSProvider) extractRecordName(fqdn, domain string) string {
}
return name
}
// txtRecordRequest represents the request body to DO's API to make a TXT record
type txtRecordRequest struct {
FieldType string `json:"fieldType"`
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
}
// txtRecordResponse represents a response from DO's API after making a TXT record
type txtRecordResponse struct {
ID int `json:"id"`
FieldType string `json:"fieldType"`
SubDomain string `json:"subDomain"`
Target string `json:"target"`
TTL int `json:"ttl"`
Zone string `json:"zone"`
}

View file

@ -16,6 +16,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/platform/config/env"
)
// Config is used to configure the creation of the DNSProvider
@ -29,11 +30,13 @@ type Config struct {
// NewDefaultConfig returns a default configuration for the DNSProvider
func NewDefaultConfig() *Config {
propagationMins := env.GetOrDefaultInt("AWS_PROPAGATION_TIMEOUT", 2)
intervalSecs := env.GetOrDefaultInt("AWS_POLLING_INTERVAL", 4)
return &Config{
MaxRetries: 5,
TTL: 10,
PropagationTimeout: time.Minute * 2,
PollingInterval: time.Second * 4,
MaxRetries: env.GetOrDefaultInt("AWS_MAX_RETRIES", 5),
TTL: env.GetOrDefaultInt("AWS_TTL", 10),
PropagationTimeout: time.Second * time.Duration(propagationMins),
PollingInterval: time.Second * time.Duration(intervalSecs),
HostedZoneID: os.Getenv("AWS_HOSTED_ZONE_ID"),
}
}