forked from TrueCloudLab/lego
Modular DNS challenge
- Manual provider - Dynamic DNS Update provider (RFC2136) - Route53 provider - CloudFlare provider
This commit is contained in:
parent
cce3d79fc9
commit
666698cea3
9 changed files with 861 additions and 11 deletions
|
@ -1,24 +1,26 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// DNSProvider represents a service for creating dns records.
|
||||||
dnsTemplate = "_acme-challenge.%s. 300 IN TXT \"%s\""
|
type DNSProvider interface {
|
||||||
)
|
// CreateTXT creates a TXT record
|
||||||
|
CreateTXTRecord(fqdn, value string, ttl int) error
|
||||||
|
RemoveTXTRecord(fqdn, value string, ttl int) error
|
||||||
|
}
|
||||||
|
|
||||||
// dnsChallenge implements the dns-01 challenge according to ACME 7.5
|
// dnsChallenge implements the dns-01 challenge according to ACME 7.5
|
||||||
type dnsChallenge struct {
|
type dnsChallenge struct {
|
||||||
jws *jws
|
jws *jws
|
||||||
|
provider DNSProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||||
|
@ -36,11 +38,10 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||||
// of the base64 encoding mentioned by the spec. Fix this if either the spec or boulder changes!
|
// of the base64 encoding mentioned by the spec. Fix this if either the spec or boulder changes!
|
||||||
keyAuthSha := hex.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
keyAuthSha := hex.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
||||||
|
|
||||||
dnsRecord := fmt.Sprintf(dnsTemplate, domain, keyAuthSha)
|
fqdn := fmt.Sprintf("_acme-challenge.%s.", domain)
|
||||||
logf("[DEBUG] acme: DNS Record: %s", dnsRecord)
|
if err = s.provider.CreateTXTRecord(fqdn, keyAuthSha, 120); err != nil {
|
||||||
|
return err
|
||||||
reader := bufio.NewReader(os.Stdin)
|
}
|
||||||
_, _ = reader.ReadString('\n')
|
|
||||||
|
|
||||||
jsonBytes, err := json.Marshal(challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
jsonBytes, err := json.Marshal(challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -83,5 +84,9 @@ Loop:
|
||||||
resp, err = http.Get(chlng.URI)
|
resp, err = http.Get(chlng.URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = s.provider.RemoveTXTRecord(fqdn, keyAuthSha, 120); err != nil {
|
||||||
|
logf("[WARN] acme: Failed to cleanup DNS record. -> %v ", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
158
acme/dns_challenge_cloudflare.go
Normal file
158
acme/dns_challenge_cloudflare.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/crackcomm/cloudflare"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProviderCloudFlare is an implementation of the DNSProvider interface
|
||||||
|
type DNSProviderCloudFlare struct {
|
||||||
|
client *cloudflare.Client
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderCloudFlare returns a DNSProviderCloudFlare instance with a configured cloudflare client.
|
||||||
|
// Authentication is either done using the passed credentials or - when empty - using the environment
|
||||||
|
// variables CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY.
|
||||||
|
func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProviderCloudFlare, error) {
|
||||||
|
if cloudflareEmail == "" || cloudflareKey == "" {
|
||||||
|
cloudflareEmail, cloudflareKey = envAuth()
|
||||||
|
if cloudflareEmail == "" || cloudflareKey == "" {
|
||||||
|
return nil, fmt.Errorf("CloudFlare credentials missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &DNSProviderCloudFlare{
|
||||||
|
client: cloudflare.New(&cloudflare.Options{cloudflareEmail, cloudflareKey}),
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTXTRecord creates a TXT record using the specified parameters
|
||||||
|
func (c *DNSProviderCloudFlare) CreateTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
zoneID, err := c.getHostedZoneID(fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
record := newTxtRecord(zoneID, fqdn, value, ttl)
|
||||||
|
err = c.client.Records.Create(c.ctx, record)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CloudFlare API call failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTXTRecord removes the TXT record matching the specified parameters
|
||||||
|
func (c *DNSProviderCloudFlare) RemoveTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
records, err := c.findTxtRecords(fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
err := c.client.Records.Delete(c.ctx, rec.ZoneID, rec.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CloudFlare API call has failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DNSProviderCloudFlare) findTxtRecords(fqdn string) ([]*cloudflare.Record, error) {
|
||||||
|
zoneID, err := c.getHostedZoneID(fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []*cloudflare.Record
|
||||||
|
result, err := c.client.Records.List(c.ctx, zoneID)
|
||||||
|
if err != nil {
|
||||||
|
return records, fmt.Errorf("CloudFlare API call has failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := unFqdn(fqdn)
|
||||||
|
for _, rec := range result {
|
||||||
|
if rec.Name == name && rec.Type == "TXT" {
|
||||||
|
records = append(records, rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) {
|
||||||
|
zones, err := c.client.Zones.List(c.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("CloudFlare API call failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostedZone cloudflare.Zone
|
||||||
|
for _, zone := range zones {
|
||||||
|
name := toFqdn(zone.Name)
|
||||||
|
if strings.HasSuffix(fqdn, name) {
|
||||||
|
if len(zone.Name) > len(hostedZone.Name) {
|
||||||
|
hostedZone = *zone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hostedZone.ID == "" {
|
||||||
|
return "", fmt.Errorf("No matching CloudFlare zone found for domain %s", fqdn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostedZone.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTxtRecord(zoneID, fqdn, value string, ttl int) *cloudflare.Record {
|
||||||
|
name := unFqdn(fqdn)
|
||||||
|
return &cloudflare.Record{
|
||||||
|
Type: "TXT",
|
||||||
|
Name: name,
|
||||||
|
Content: value,
|
||||||
|
TTL: sanitizeTTL(ttl),
|
||||||
|
ZoneID: zoneID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFqdn(name string) string {
|
||||||
|
n := len(name)
|
||||||
|
if n == 0 || name[n-1] == '.' {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return name + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
func unFqdn(name string) string {
|
||||||
|
n := len(name)
|
||||||
|
if n != 0 && name[n-1] == '.' {
|
||||||
|
return name[:n-1]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTL must be between 120 and 86400 seconds
|
||||||
|
func sanitizeTTL(ttl int) int {
|
||||||
|
if ttl < 120 {
|
||||||
|
ttl = 120
|
||||||
|
} else if ttl > 86400 {
|
||||||
|
ttl = 86400
|
||||||
|
}
|
||||||
|
return ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
func envAuth() (email, apiKey string) {
|
||||||
|
email = os.Getenv("CLOUDFLARE_EMAIL")
|
||||||
|
apiKey = os.Getenv("CLOUDFLARE_API_KEY")
|
||||||
|
if len(email) == 0 || len(apiKey) == 0 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
83
acme/dns_challenge_cloudflare_test.go
Normal file
83
acme/dns_challenge_cloudflare_test.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cflareLiveTest bool
|
||||||
|
cflareEmail string
|
||||||
|
cflareAPIKey string
|
||||||
|
cflareDomain string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cflareEmail = os.Getenv("CLOUDFLARE_EMAIL")
|
||||||
|
cflareAPIKey = os.Getenv("CLOUDFLARE_API_KEY")
|
||||||
|
cflareDomain = os.Getenv("CLOUDFLARE_DOMAIN")
|
||||||
|
if len(cflareEmail) > 0 && len(cflareAPIKey) > 0 && len(cflareDomain) > 0 {
|
||||||
|
cflareLiveTest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreCloudFlareEnv() {
|
||||||
|
os.Setenv("CLOUDFLARE_EMAIL", cflareEmail)
|
||||||
|
os.Setenv("CLOUDFLARE_API_KEY", cflareAPIKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderCloudFlareValid(t *testing.T) {
|
||||||
|
os.Setenv("CLOUDFLARE_EMAIL", "")
|
||||||
|
os.Setenv("CLOUDFLARE_API_KEY", "")
|
||||||
|
_, err := NewDNSProviderCloudFlare("123", "123")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
restoreCloudFlareEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderCloudFlareValidEnv(t *testing.T) {
|
||||||
|
os.Setenv("CLOUDFLARE_EMAIL", "test@example.com")
|
||||||
|
os.Setenv("CLOUDFLARE_API_KEY", "123")
|
||||||
|
_, err := NewDNSProviderCloudFlare("", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
restoreCloudFlareEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderCloudFlareMissingCredErr(t *testing.T) {
|
||||||
|
os.Setenv("CLOUDFLARE_EMAIL", "")
|
||||||
|
os.Setenv("CLOUDFLARE_API_KEY", "")
|
||||||
|
_, err := NewDNSProviderCloudFlare("", "")
|
||||||
|
assert.EqualError(t, err, "CloudFlare credentials missing")
|
||||||
|
restoreCloudFlareEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudFlareCreateTXTRecord(t *testing.T) {
|
||||||
|
if !cflareLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fqdn := fmt.Sprintf("_acme-challenge.123.%s.", cflareDomain)
|
||||||
|
err = provider.CreateTXTRecord(fqdn, "123d==", 120)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloudFlareRemoveTXTRecord(t *testing.T) {
|
||||||
|
if !cflareLiveTest {
|
||||||
|
t.Skip("skipping live test")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fqdn := fmt.Sprintf("_acme-challenge.123.%s.", cflareDomain)
|
||||||
|
err = provider.RemoveTXTRecord(fqdn, "123d==", 120)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
38
acme/dns_challenge_manual.go
Normal file
38
acme/dns_challenge_manual.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsTemplate = "%s %d IN TXT \"%s\""
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProviderManual is an implementation of the DNSProvider interface
|
||||||
|
type DNSProviderManual struct{}
|
||||||
|
|
||||||
|
// NewDNSProviderManual returns a DNSProviderManual instance.
|
||||||
|
func NewDNSProviderManual() (*DNSProviderManual, error) {
|
||||||
|
return &DNSProviderManual{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTXTRecord prints instructions for manually creating the TXT record
|
||||||
|
func (*DNSProviderManual) CreateTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
|
||||||
|
logf("[INFO] acme: Please create the following TXT record in your DNS zone:")
|
||||||
|
logf("[INFO] acme: %s", dnsRecord)
|
||||||
|
logf("[INFO] acme: Press 'Enter' when you are done")
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
_, _ = reader.ReadString('\n')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTXTRecord prints instructions for manually removing the TXT record
|
||||||
|
func (*DNSProviderManual) RemoveTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
|
||||||
|
logf("[INFO] acme: You can now remove this TXT record from your DNS zone:")
|
||||||
|
logf("[INFO] acme: %s", dnsRecord)
|
||||||
|
return nil
|
||||||
|
}
|
84
acme/dns_challenge_rfc2136.go
Normal file
84
acme/dns_challenge_rfc2136.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProviderRFC2136 is an implementation of the DNSProvider interface that
|
||||||
|
// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
|
||||||
|
type DNSProviderRFC2136 struct {
|
||||||
|
nameserver string
|
||||||
|
zone string
|
||||||
|
tsigKey string
|
||||||
|
tsigSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance.
|
||||||
|
// To disable TSIG authentication 'tsigKey' and 'tsigSecret' must be set to the empty string.
|
||||||
|
// 'nameserver' must be a network address in the the form "host:port". 'zone' must be the fully
|
||||||
|
// qualified name of the zone.
|
||||||
|
func NewDNSProviderRFC2136(nameserver, zone, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) {
|
||||||
|
d := &DNSProviderRFC2136{
|
||||||
|
nameserver: nameserver,
|
||||||
|
zone: zone,
|
||||||
|
}
|
||||||
|
if len(tsigKey) > 0 && len(tsigSecret) > 0 {
|
||||||
|
d.tsigKey = tsigKey
|
||||||
|
d.tsigSecret = tsigSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTXTRecord creates a TXT record using the specified parameters
|
||||||
|
func (r *DNSProviderRFC2136) CreateTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
return r.changeRecord("INSERT", fqdn, value, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTXTRecord removes the TXT record matching the specified parameters
|
||||||
|
func (r *DNSProviderRFC2136) RemoveTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
return r.changeRecord("REMOVE", fqdn, value, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) error {
|
||||||
|
// Create RR
|
||||||
|
rr := new(dns.TXT)
|
||||||
|
rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)}
|
||||||
|
rr.Txt = []string{value}
|
||||||
|
rrs := make([]dns.RR, 1)
|
||||||
|
rrs[0] = rr
|
||||||
|
|
||||||
|
// Create dynamic update packet
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetUpdate(dns.Fqdn(r.zone))
|
||||||
|
switch action {
|
||||||
|
case "INSERT":
|
||||||
|
m.Insert(rrs)
|
||||||
|
case "REMOVE":
|
||||||
|
m.Remove(rrs)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unexpected action: %s", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup client
|
||||||
|
c := new(dns.Client)
|
||||||
|
c.SingleInflight = true
|
||||||
|
// TSIG authentication / msg signing
|
||||||
|
if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 {
|
||||||
|
m.SetTsig(dns.Fqdn(r.tsigKey), dns.HmacMD5, 300, time.Now().Unix())
|
||||||
|
c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the query
|
||||||
|
reply, _, err := c.Exchange(m, r.nameserver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DNS update failed: %v", err)
|
||||||
|
}
|
||||||
|
if reply != nil && reply.Rcode != dns.RcodeSuccess {
|
||||||
|
return fmt.Errorf("DNS update failed. Server replied: %s", dns.RcodeToString[reply.Rcode])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
227
acme/dns_challenge_rfc2136_test.go
Normal file
227
acme/dns_challenge_rfc2136_test.go
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rfc2136TestValue = "so6ZGir4GaZqI11h9UccBB=="
|
||||||
|
rfc2136TestFqdn = "_acme-challenge.123456789.www.example.com."
|
||||||
|
rfc2136TestZone = "example.com."
|
||||||
|
rfc2136TestTTL = 120
|
||||||
|
rfc2136TestTsigKey = "example.com."
|
||||||
|
rfc2136TestTsigSecret = "IwBTJx9wrDp4Y1RyC3H0gA=="
|
||||||
|
)
|
||||||
|
|
||||||
|
var reqChan = make(chan *dns.Msg, 10)
|
||||||
|
|
||||||
|
func TestRFC2136CanaryLocalTestServer(t *testing.T) {
|
||||||
|
dns.HandleFunc("example.com.", serverHandlerHello)
|
||||||
|
defer dns.HandleRemove("example.com.")
|
||||||
|
|
||||||
|
server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start test server: %v", err)
|
||||||
|
}
|
||||||
|
defer server.Shutdown()
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("example.com.", dns.TypeTXT)
|
||||||
|
r, _, err := c.Exchange(m, addrstr)
|
||||||
|
if err != nil || len(r.Extra) == 0 {
|
||||||
|
t.Fatalf("Failed to communicate with test server:", err)
|
||||||
|
}
|
||||||
|
txt := r.Extra[0].(*dns.TXT).Txt[0]
|
||||||
|
if txt != "Hello world" {
|
||||||
|
t.Error("Expected test server to return 'Hello world' but got: ", txt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRFC2136ServerSuccess(t *testing.T) {
|
||||||
|
dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
|
||||||
|
defer dns.HandleRemove(rfc2136TestZone)
|
||||||
|
|
||||||
|
server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start test server: %v", err)
|
||||||
|
}
|
||||||
|
defer server.Shutdown()
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err != nil {
|
||||||
|
t.Errorf("Expected CreateTXTRecord() to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRFC2136ServerError(t *testing.T) {
|
||||||
|
dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr)
|
||||||
|
defer dns.HandleRemove(rfc2136TestZone)
|
||||||
|
|
||||||
|
server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start test server: %v", err)
|
||||||
|
}
|
||||||
|
defer server.Shutdown()
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err == nil {
|
||||||
|
t.Errorf("Expected CreateTXTRecord() to return an error but it did not.")
|
||||||
|
} else if !strings.Contains(err.Error(), "NOTZONE") {
|
||||||
|
t.Errorf("Expected CreateTXTRecord() to return an error with the 'NOTZONE' rcode string but it did not.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRFC2136TsigClient(t *testing.T) {
|
||||||
|
dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
|
||||||
|
defer dns.HandleRemove(rfc2136TestZone)
|
||||||
|
|
||||||
|
server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start test server: %v", err)
|
||||||
|
}
|
||||||
|
defer server.Shutdown()
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, rfc2136TestTsigKey, rfc2136TestTsigSecret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err != nil {
|
||||||
|
t.Errorf("Expected CreateTXTRecord() to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRFC2136ValidUpdatePacket(t *testing.T) {
|
||||||
|
dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest)
|
||||||
|
defer dns.HandleRemove(rfc2136TestZone)
|
||||||
|
|
||||||
|
server, addrstr, err := runLocalDNSTestServer("127.0.0.1:0", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start test server: %v", err)
|
||||||
|
}
|
||||||
|
defer server.Shutdown()
|
||||||
|
|
||||||
|
rr := new(dns.TXT)
|
||||||
|
rr.Hdr = dns.RR_Header{
|
||||||
|
Name: rfc2136TestFqdn,
|
||||||
|
Rrtype: dns.TypeTXT,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: uint32(rfc2136TestTTL),
|
||||||
|
}
|
||||||
|
rr.Txt = []string{rfc2136TestValue}
|
||||||
|
rrs := make([]dns.RR, 1)
|
||||||
|
rrs[0] = rr
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetUpdate(dns.Fqdn(rfc2136TestZone))
|
||||||
|
m.Insert(rrs)
|
||||||
|
expectstr := m.String()
|
||||||
|
expect, err := m.Pack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error packing expect msg: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
if err := provider.CreateTXTRecord(rfc2136TestFqdn, rfc2136TestValue, rfc2136TestTTL); err != nil {
|
||||||
|
t.Errorf("Expected CreateTXTRecord() to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rcvMsg := <-reqChan
|
||||||
|
rcvMsg.Id = m.Id
|
||||||
|
actual, err := rcvMsg.Pack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error packing actual msg: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(actual, expect) {
|
||||||
|
tmp := new(dns.Msg)
|
||||||
|
if err := tmp.Unpack(actual); err != nil {
|
||||||
|
t.Fatalf("Error unpacking actual msg: %v", err)
|
||||||
|
}
|
||||||
|
t.Errorf("Expected msg:\n%s", expectstr)
|
||||||
|
t.Errorf("Actual msg:\n%v", tmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLocalDNSTestServer(listenAddr string, tsig bool) (*dns.Server, string, error) {
|
||||||
|
pc, err := net.ListenPacket("udp", listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
server := &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
|
||||||
|
if tsig {
|
||||||
|
server.TsigSecret = map[string]string{rfc2136TestTsigKey: rfc2136TestTsigSecret}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitLock := sync.Mutex{}
|
||||||
|
waitLock.Lock()
|
||||||
|
server.NotifyStartedFunc = waitLock.Unlock
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
server.ActivateAndServe()
|
||||||
|
pc.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitLock.Lock()
|
||||||
|
return server, pc.LocalAddr().String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(req)
|
||||||
|
m.Extra = make([]dns.RR, 1)
|
||||||
|
m.Extra[0] = &dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0},
|
||||||
|
Txt: []string{"Hello world"},
|
||||||
|
}
|
||||||
|
w.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(req)
|
||||||
|
|
||||||
|
if t := req.IsTsig(); t != nil {
|
||||||
|
if w.TsigStatus() == nil {
|
||||||
|
// Validated
|
||||||
|
m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetRcode(req, dns.RcodeNotZone)
|
||||||
|
w.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(req)
|
||||||
|
|
||||||
|
if t := req.IsTsig(); t != nil {
|
||||||
|
if w.TsigStatus() == nil {
|
||||||
|
// Validated
|
||||||
|
m.SetTsig(rfc2136TestZone, dns.HmacMD5, 300, time.Now().Unix())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteMsg(m)
|
||||||
|
reqChan <- req
|
||||||
|
}
|
93
acme/dns_challenge_route53.go
Normal file
93
acme/dns_challenge_route53.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/goamz/aws"
|
||||||
|
"github.com/mitchellh/goamz/route53"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSProviderRoute53 is an implementation of the DNSProvider interface
|
||||||
|
type DNSProviderRoute53 struct {
|
||||||
|
client *route53.Route53
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderRoute53 returns a DNSProviderRoute53 instance with a configured route53 client.
|
||||||
|
// Authentication is either done using the passed credentials or - when empty -
|
||||||
|
// using the environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
|
||||||
|
func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*DNSProviderRoute53, error) {
|
||||||
|
region, ok := aws.Regions[awsRegionName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Invalid AWS region name %s", awsRegionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var auth aws.Auth
|
||||||
|
// First try passed in credentials
|
||||||
|
if awsAccessKey != "" && awsSecretKey != "" {
|
||||||
|
auth = aws.Auth{awsAccessKey, awsSecretKey, ""}
|
||||||
|
} else {
|
||||||
|
// try getting credentials from environment
|
||||||
|
envAuth, err := aws.EnvAuth()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AWS credentials missing")
|
||||||
|
}
|
||||||
|
auth = envAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
client := route53.New(auth, region)
|
||||||
|
return &DNSProviderRoute53{client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTXTRecord creates a TXT record using the specified parameters
|
||||||
|
func (r *DNSProviderRoute53) CreateTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
return r.changeRecord("UPSERT", fqdn, value, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTXTRecord removes the TXT record matching the specified parameters
|
||||||
|
func (r *DNSProviderRoute53) RemoveTXTRecord(fqdn, value string, ttl int) error {
|
||||||
|
return r.changeRecord("DELETE", fqdn, value, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) error {
|
||||||
|
hostedZoneID, err := r.getHostedZoneID(fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recordSet := newTXTRecordSet(fqdn, value, ttl)
|
||||||
|
update := route53.Change{action, recordSet}
|
||||||
|
changes := []route53.Change{update}
|
||||||
|
req := route53.ChangeResourceRecordSetsRequest{Comment: "Created by Lego", Changes: changes}
|
||||||
|
_, err = r.client.ChangeResourceRecordSets(hostedZoneID, &req)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DNSProviderRoute53) getHostedZoneID(fqdn string) (string, error) {
|
||||||
|
zoneResp, err := r.client.ListHostedZones("", math.MaxInt64)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var hostedZone route53.HostedZone
|
||||||
|
for _, zone := range zoneResp.HostedZones {
|
||||||
|
//if strings.HasSuffix(domain, strings.Trim(zone.Name, ".")) {
|
||||||
|
if strings.HasSuffix(fqdn, zone.Name) {
|
||||||
|
if len(zone.Name) > len(hostedZone.Name) {
|
||||||
|
hostedZone = zone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hostedZone.ID == "" {
|
||||||
|
return "", fmt.Errorf("No Route53 zone found for domain %s", fqdn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostedZone.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTXTRecordSet(fqdn, value string, ttl int) route53.ResourceRecordSet {
|
||||||
|
return route53.ResourceRecordSet{
|
||||||
|
Name: fqdn,
|
||||||
|
Type: "TXT",
|
||||||
|
Records: []string{value},
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
}
|
126
acme/dns_challenge_route53_test.go
Normal file
126
acme/dns_challenge_route53_test.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/goamz/aws"
|
||||||
|
"github.com/mitchellh/goamz/route53"
|
||||||
|
"github.com/mitchellh/goamz/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
route53Secret string
|
||||||
|
route53Key string
|
||||||
|
testServer *testutil.HTTPServer
|
||||||
|
)
|
||||||
|
|
||||||
|
var ChangeResourceRecordSetsAnswer = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
||||||
|
<ChangeInfo>
|
||||||
|
<Id>/change/asdf</Id>
|
||||||
|
<Status>PENDING</Status>
|
||||||
|
<SubmittedAt>2014</SubmittedAt>
|
||||||
|
</ChangeInfo>
|
||||||
|
</ChangeResourceRecordSetsResponse>`
|
||||||
|
|
||||||
|
var ListHostedZonesAnswer = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ListHostedZonesResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
||||||
|
<HostedZones>
|
||||||
|
<HostedZone>
|
||||||
|
<Id>/hostedzone/Z2K123214213123</Id>
|
||||||
|
<Name>example.com.</Name>
|
||||||
|
<CallerReference>D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A</CallerReference>
|
||||||
|
<Config>
|
||||||
|
<Comment>Test comment</Comment>
|
||||||
|
</Config>
|
||||||
|
<ResourceRecordSetCount>10</ResourceRecordSetCount>
|
||||||
|
</HostedZone>
|
||||||
|
<HostedZone>
|
||||||
|
<Id>/hostedzone/ZLT12321321124</Id>
|
||||||
|
<Name>sub.example.com.</Name>
|
||||||
|
<CallerReference>A970F076-FCB1-D959-B395-96474CC84EB8</CallerReference>
|
||||||
|
<Config>
|
||||||
|
<Comment>Test comment for subdomain host</Comment>
|
||||||
|
</Config>
|
||||||
|
<ResourceRecordSetCount>4</ResourceRecordSetCount>
|
||||||
|
</HostedZone>
|
||||||
|
</HostedZones>
|
||||||
|
<IsTruncated>false</IsTruncated>
|
||||||
|
<MaxItems>100</MaxItems>
|
||||||
|
</ListHostedZonesResponse>`
|
||||||
|
|
||||||
|
var serverResponseMap = testutil.ResponseMap{
|
||||||
|
"/2013-04-01/hostedzone/": testutil.Response{200, nil, ListHostedZonesAnswer},
|
||||||
|
"/2013-04-01/hostedzone/Z2K123214213123/rrset": testutil.Response{200, nil, ChangeResourceRecordSetsAnswer},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
route53Key = os.Getenv("AWS_ACCESS_KEY_ID")
|
||||||
|
route53Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
||||||
|
testServer = testutil.NewHTTPServer()
|
||||||
|
testServer.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreRoute53Env() {
|
||||||
|
os.Setenv("AWS_ACCESS_KEY_ID", route53Key)
|
||||||
|
os.Setenv("AWS_SECRET_ACCESS_KEY", route53Secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRoute53TestServer() *testutil.HTTPServer {
|
||||||
|
testServer.Flush()
|
||||||
|
return testServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRoute53Provider(server *testutil.HTTPServer) *DNSProviderRoute53 {
|
||||||
|
auth := aws.Auth{"abc", "123", ""}
|
||||||
|
client := route53.NewWithClient(auth, aws.Region{Route53Endpoint: server.URL}, testutil.DefaultClient)
|
||||||
|
return &DNSProviderRoute53{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderRoute53Valid(t *testing.T) {
|
||||||
|
os.Setenv("AWS_ACCESS_KEY_ID", "")
|
||||||
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
||||||
|
_, err := NewDNSProviderRoute53("123", "123", "us-east-1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
restoreRoute53Env()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderRoute53ValidEnv(t *testing.T) {
|
||||||
|
os.Setenv("AWS_ACCESS_KEY_ID", "123")
|
||||||
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "123")
|
||||||
|
_, err := NewDNSProviderRoute53("", "", "us-east-1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
restoreRoute53Env()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderRoute53MissingAuthErr(t *testing.T) {
|
||||||
|
os.Setenv("AWS_ACCESS_KEY_ID", "")
|
||||||
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
||||||
|
_, err := NewDNSProviderRoute53("", "", "us-east-1")
|
||||||
|
assert.EqualError(t, err, "AWS credentials missing")
|
||||||
|
restoreRoute53Env()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderRoute53InvalidRegionErr(t *testing.T) {
|
||||||
|
_, err := NewDNSProviderRoute53("123", "123", "us-east-3")
|
||||||
|
assert.EqualError(t, err, "Invalid AWS region name us-east-3")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoute53CreateTXTRecord(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
testServer := makeRoute53TestServer()
|
||||||
|
provider := makeRoute53Provider(testServer)
|
||||||
|
testServer.ResponseMap(2, serverResponseMap)
|
||||||
|
|
||||||
|
err := provider.CreateTXTRecord("_acme-challenge.123.example.com.", "123456d==", 120)
|
||||||
|
assert.NoError(err, "Expected CreateTXTRecord to return no error")
|
||||||
|
|
||||||
|
httpReqs := testServer.WaitRequests(2)
|
||||||
|
httpReq := httpReqs[1]
|
||||||
|
|
||||||
|
assert.Equal("/2013-04-01/hostedzone/Z2K123214213123/rrset", httpReq.URL.Path,
|
||||||
|
"Expected CreateTXTRecord to select the correct hostedzone")
|
||||||
|
|
||||||
|
}
|
36
acme/dns_challenge_test.go
Normal file
36
acme/dns_challenge_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/rsa"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDNSValidServerResponse(t *testing.T) {
|
||||||
|
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("Replay-Nonce", "12345")
|
||||||
|
w.Write([]byte("{\"type\":\"dns01\",\"status\":\"valid\",\"uri\":\"http://some.url\",\"token\":\"http8\"}"))
|
||||||
|
}))
|
||||||
|
|
||||||
|
manualProvider, _ := NewDNSProviderManual()
|
||||||
|
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
|
||||||
|
solver := &dnsChallenge{jws: jws, provider: manualProvider}
|
||||||
|
clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
f := bufio.NewWriter(os.Stdout)
|
||||||
|
defer f.Flush()
|
||||||
|
f.WriteString("\n")
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := solver.Solve(clientChallenge, "example.com"); err != nil {
|
||||||
|
t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue