forked from TrueCloudLab/lego
ACME V2 support (#555)
This commit is contained in:
parent
1d9b0906b1
commit
e7fd871a9c
68 changed files with 1637 additions and 1819 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,7 +1,9 @@
|
||||||
lego.exe
|
lego.exe
|
||||||
lego
|
lego
|
||||||
.lego
|
.lego
|
||||||
|
.gitcookies
|
||||||
.idea
|
.idea
|
||||||
|
.vscode/
|
||||||
dist/
|
dist/
|
||||||
builds/
|
builds/
|
||||||
.gitcookies
|
vendor/
|
||||||
|
|
|
@ -44,7 +44,6 @@ yaourt -S lego-git
|
||||||
- Revoke certificates
|
- Revoke certificates
|
||||||
- Robust implementation of all ACME challenges
|
- Robust implementation of all ACME challenges
|
||||||
- HTTP (http-01)
|
- HTTP (http-01)
|
||||||
- TLS with Server Name Indication (tls-sni-01)
|
|
||||||
- DNS (dns-01)
|
- DNS (dns-01)
|
||||||
- SAN certificate support
|
- SAN certificate support
|
||||||
- Comes with multiple optional [DNS providers](https://github.com/xenolf/lego/tree/master/providers/dns)
|
- Comes with multiple optional [DNS providers](https://github.com/xenolf/lego/tree/master/providers/dns)
|
||||||
|
@ -107,15 +106,14 @@ GLOBAL OPTIONS:
|
||||||
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
|
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
|
||||||
--key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048")
|
--key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048")
|
||||||
--path value Directory to use for storing the data (default: "/.lego")
|
--path value Directory to use for storing the data (default: "/.lego")
|
||||||
--exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01".
|
--exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "dns-01",.
|
||||||
--webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge
|
--webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge
|
||||||
--memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.
|
--memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.
|
||||||
--http value Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port
|
--http value Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port
|
||||||
--tls value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port
|
|
||||||
--dns value Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.
|
--dns value Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.
|
||||||
--http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
|
--http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
|
||||||
--dns-timeout value Set the DNS timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
|
--dns-timeout value Set the DNS timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
|
||||||
--dns-resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use Google's DNS resolvers.
|
--dns-resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
|
||||||
--pem Generate a .pem file by concatanating the .key and .crt files together.
|
--pem Generate a .pem file by concatanating the .key and .crt files together.
|
||||||
--help, -h show help
|
--help, -h show help
|
||||||
--version, -v print the version
|
--version, -v print the version
|
||||||
|
@ -152,7 +150,7 @@ Obtain a certificate using the DNS challenge and AWS Route 53:
|
||||||
$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run
|
$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead.
|
Note that `--dns=foo` implies `--exclude=http-01`. lego will not attempt other challenges if you've told it to use DNS instead.
|
||||||
|
|
||||||
Obtain a certificate given a certificate signing request (CSR) generated by something else:
|
Obtain a certificate given a certificate signing request (CSR) generated by something else:
|
||||||
|
|
||||||
|
|
45
account.go
45
account.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account represents a users local saved credentials
|
// Account represents a users local saved credentials
|
||||||
|
@ -25,23 +26,23 @@ func NewAccount(email string, conf *Configuration) *Account {
|
||||||
// TODO: move to function in configuration?
|
// TODO: move to function in configuration?
|
||||||
accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key"
|
accKeyPath := accKeysPath + string(os.PathSeparator) + email + ".key"
|
||||||
if err := checkFolder(accKeysPath); err != nil {
|
if err := checkFolder(accKeysPath); err != nil {
|
||||||
logger().Fatalf("Could not check/create directory for account %s: %v", email, err)
|
log.Fatalf("Could not check/create directory for account %s: %v", email, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var privKey crypto.PrivateKey
|
var privKey crypto.PrivateKey
|
||||||
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
|
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
|
||||||
|
|
||||||
logger().Printf("No key found for account %s. Generating a curve P384 EC key.", email)
|
log.Printf("No key found for account %s. Generating a curve P384 EC key.", email)
|
||||||
privKey, err = generatePrivateKey(accKeyPath)
|
privKey, err = generatePrivateKey(accKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err)
|
log.Fatalf("Could not generate RSA private account key for account %s: %v", email, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger().Printf("Saved key to %s", accKeyPath)
|
log.Printf("Saved key to %s", accKeyPath)
|
||||||
} else {
|
} else {
|
||||||
privKey, err = loadPrivateKey(accKeyPath)
|
privKey, err = loadPrivateKey(accKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err)
|
log.Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,29 +53,53 @@ func NewAccount(email string, conf *Configuration) *Account {
|
||||||
|
|
||||||
fileBytes, err := ioutil.ReadFile(accountFile)
|
fileBytes, err := ioutil.ReadFile(accountFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not load file for account %s -> %v", email, err)
|
log.Fatalf("Could not load file for account %s -> %v", email, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var acc Account
|
var acc Account
|
||||||
err = json.Unmarshal(fileBytes, &acc)
|
err = json.Unmarshal(fileBytes, &acc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not parse file for account %s -> %v", email, err)
|
log.Fatalf("Could not parse file for account %s -> %v", email, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.key = privKey
|
acc.key = privKey
|
||||||
acc.conf = conf
|
acc.conf = conf
|
||||||
|
|
||||||
if acc.Registration == nil {
|
if acc.Registration == nil || acc.Registration.Body.Status == "" {
|
||||||
logger().Fatalf("Could not load account for %s. Registration is nil.", email)
|
reg, err := tryRecoverAccount(privKey, conf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not load account for %s. Registration is nil -> %#v", email, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.Registration = reg
|
||||||
|
err = acc.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not save account for %s. Registration is nil -> %#v", email, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if acc.conf == nil {
|
if acc.conf == nil {
|
||||||
logger().Fatalf("Could not load account for %s. Configuration is nil.", email)
|
log.Fatalf("Could not load account for %s. Configuration is nil.", email)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &acc
|
return &acc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tryRecoverAccount(privKey crypto.PrivateKey, conf *Configuration) (*acme.RegistrationResource, error) {
|
||||||
|
// couldn't load account but got a key. Try to look the account up.
|
||||||
|
serverURL := conf.context.GlobalString("server")
|
||||||
|
client, err := acme.NewClient(serverURL, &Account{key: privKey, conf: conf}, acme.RSA2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reg, err := client.ResolveAccountByKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reg, nil
|
||||||
|
}
|
||||||
|
|
||||||
/** Implementation of the acme.User interface **/
|
/** Implementation of the acme.User interface **/
|
||||||
|
|
||||||
// GetEmail returns the email address for the account
|
// GetEmail returns the email address for the account
|
||||||
|
|
|
@ -7,9 +7,6 @@ const (
|
||||||
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
|
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
|
||||||
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
|
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
|
||||||
HTTP01 = Challenge("http-01")
|
HTTP01 = Challenge("http-01")
|
||||||
// TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni
|
|
||||||
// Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge
|
|
||||||
TLSSNI01 = Challenge("tls-sni-01")
|
|
||||||
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
|
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
|
||||||
// Note: DNS01Record returns a DNS record which will fulfill this challenge
|
// Note: DNS01Record returns a DNS record which will fulfill this challenge
|
||||||
DNS01 = Challenge("dns-01")
|
DNS01 = Challenge("dns-01")
|
||||||
|
|
687
acme/client.go
687
acme/client.go
File diff suppressed because it is too large
Load diff
|
@ -27,7 +27,13 @@ func TestNewClient(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
|
data, _ := json.Marshal(directory{
|
||||||
|
NewNonceURL: "http://test",
|
||||||
|
NewAccountURL: "http://test",
|
||||||
|
NewOrderURL: "http://test",
|
||||||
|
RevokeCertURL: "http://test",
|
||||||
|
KeyChangeURL: "http://test",
|
||||||
|
})
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -47,7 +53,7 @@ func TestNewClient(t *testing.T) {
|
||||||
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
|
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected, actual := 2, len(client.solvers); actual != expected {
|
if expected, actual := 1, len(client.solvers); actual != expected {
|
||||||
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
|
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +71,13 @@ func TestClientOptPort(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
|
data, _ := json.Marshal(directory{
|
||||||
|
NewNonceURL: "http://test",
|
||||||
|
NewAccountURL: "http://test",
|
||||||
|
NewOrderURL: "http://test",
|
||||||
|
RevokeCertURL: "http://test",
|
||||||
|
KeyChangeURL: "http://test",
|
||||||
|
})
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -76,7 +88,6 @@ func TestClientOptPort(t *testing.T) {
|
||||||
t.Fatalf("Could not create client: %v", err)
|
t.Fatalf("Could not create client: %v", err)
|
||||||
}
|
}
|
||||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
||||||
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
|
|
||||||
|
|
||||||
httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
|
httpSolver, ok := client.solvers[HTTP01].(*httpChallenge)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -92,31 +103,13 @@ func TestClientOptPort(t *testing.T) {
|
||||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
|
|
||||||
}
|
|
||||||
if httpsSolver.jws != client.jws {
|
|
||||||
t.Error("Expected tls-sni-01 to have same jws as client")
|
|
||||||
}
|
|
||||||
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
|
|
||||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
|
|
||||||
}
|
|
||||||
if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost {
|
|
||||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test setting different host
|
// test setting different host
|
||||||
optHost = "127.0.0.1"
|
optHost = "127.0.0.1"
|
||||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
||||||
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
|
|
||||||
|
|
||||||
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
||||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
||||||
}
|
}
|
||||||
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
|
|
||||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
||||||
|
@ -124,12 +117,12 @@ func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) {
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
w.Header().Add("Replay-Nonce", "12345")
|
||||||
w.Header().Add("Retry-After", "0")
|
w.Header().Add("Retry-After", "0")
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URI: "http://example.com/", Token: "token"})
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"})
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||||
j := &jws{privKey: privKey, directoryURL: ts.URL}
|
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
||||||
ch := make(chan bool)
|
ch := make(chan bool)
|
||||||
resultCh := make(chan bool)
|
resultCh := make(chan bool)
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -163,12 +156,12 @@ func TestValidate(t *testing.T) {
|
||||||
case "POST":
|
case "POST":
|
||||||
st := statuses[0]
|
st := statuses[0]
|
||||||
statuses = statuses[1:]
|
statuses = statuses[1:]
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
||||||
|
|
||||||
case "GET":
|
case "GET":
|
||||||
st := statuses[0]
|
st := statuses[0]
|
||||||
statuses = statuses[1:]
|
statuses = statuses[1:]
|
||||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
|
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http.Error(w, r.Method, http.StatusMethodNotAllowed)
|
http.Error(w, r.Method, http.StatusMethodNotAllowed)
|
||||||
|
@ -177,7 +170,7 @@ func TestValidate(t *testing.T) {
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||||
j := &jws{privKey: privKey, directoryURL: ts.URL}
|
j := &jws{privKey: privKey, getNonceURL: ts.URL}
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -186,10 +179,10 @@ func TestValidate(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"POST-unexpected", []string{"weird"}, "unexpected"},
|
{"POST-unexpected", []string{"weird"}, "unexpected"},
|
||||||
{"POST-valid", []string{"valid"}, ""},
|
{"POST-valid", []string{"valid"}, ""},
|
||||||
{"POST-invalid", []string{"invalid"}, "Error Detail"},
|
{"POST-invalid", []string{"invalid"}, "Error"},
|
||||||
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
|
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
|
||||||
{"GET-valid", []string{"pending", "valid"}, ""},
|
{"GET-valid", []string{"pending", "valid"}, ""},
|
||||||
{"GET-invalid", []string{"pending", "invalid"}, "Error Detail"},
|
{"GET-invalid", []string{"pending", "invalid"}, "Error"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
|
@ -209,9 +202,15 @@ func TestGetChallenges(t *testing.T) {
|
||||||
case "GET", "HEAD":
|
case "GET", "HEAD":
|
||||||
w.Header().Add("Replay-Nonce", "12345")
|
w.Header().Add("Replay-Nonce", "12345")
|
||||||
w.Header().Add("Retry-After", "0")
|
w.Header().Add("Retry-After", "0")
|
||||||
writeJSONResponse(w, directory{NewAuthzURL: ts.URL, NewCertURL: ts.URL, NewRegURL: ts.URL, RevokeCertURL: ts.URL})
|
writeJSONResponse(w, directory{
|
||||||
|
NewNonceURL: ts.URL,
|
||||||
|
NewAccountURL: ts.URL,
|
||||||
|
NewOrderURL: ts.URL,
|
||||||
|
RevokeCertURL: ts.URL,
|
||||||
|
KeyChangeURL: ts.URL,
|
||||||
|
})
|
||||||
case "POST":
|
case "POST":
|
||||||
writeJSONResponse(w, authorization{})
|
writeJSONResponse(w, orderMessage{})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
@ -224,7 +223,7 @@ func TestGetChallenges(t *testing.T) {
|
||||||
}
|
}
|
||||||
user := mockUser{
|
user := mockUser{
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
regres: &RegistrationResource{NewAuthzURL: ts.URL},
|
regres: &RegistrationResource{URI: ts.URL},
|
||||||
privatekey: key,
|
privatekey: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,12 +232,60 @@ func TestGetChallenges(t *testing.T) {
|
||||||
t.Fatalf("Could not create client: %v", err)
|
t.Fatalf("Could not create client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, failures := client.getChallenges([]string{"example.com"})
|
_, err = client.createOrderForIdentifiers([]string{"example.com"})
|
||||||
if failures["example.com"] == nil {
|
if err != nil {
|
||||||
t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil")
|
t.Fatal("Expecting \"Server did not provide next link to proceed\" error, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveAccountByKey(t *testing.T) {
|
||||||
|
keyBits := 512
|
||||||
|
keyType := RSA2048
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Could not generate test key:", err)
|
||||||
|
}
|
||||||
|
user := mockUser{
|
||||||
|
email: "test@test.com",
|
||||||
|
regres: new(RegistrationResource),
|
||||||
|
privatekey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ts *httptest.Server
|
||||||
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.RequestURI {
|
||||||
|
case "/directory":
|
||||||
|
writeJSONResponse(w, directory{
|
||||||
|
NewNonceURL: ts.URL + "/nonce",
|
||||||
|
NewAccountURL: ts.URL + "/account",
|
||||||
|
NewOrderURL: ts.URL + "/newOrder",
|
||||||
|
RevokeCertURL: ts.URL + "/revokeCert",
|
||||||
|
KeyChangeURL: ts.URL + "/keyChange",
|
||||||
|
})
|
||||||
|
case "/nonce":
|
||||||
|
w.Header().Add("Replay-Nonce", "12345")
|
||||||
|
w.Header().Add("Retry-After", "0")
|
||||||
|
case "/account":
|
||||||
|
w.Header().Set("Location", ts.URL+"/account_recovery")
|
||||||
|
case "/account_recovery":
|
||||||
|
writeJSONResponse(w, accountMessage{
|
||||||
|
Status: "valid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
client, err := NewClient(ts.URL+"/directory", user, keyType)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Could not create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := client.ResolveAccountByKey(); err != nil {
|
||||||
|
t.Fatalf("Unexpected error resolving account by key: %v", err)
|
||||||
|
} else if res.Body.Status != "valid" {
|
||||||
|
t.Errorf("Unexpected account status: %v", res.Body.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// writeJSONResponse marshals the body as JSON and writes it to the response.
|
// writeJSONResponse marshals the body as JSON and writes it to the response.
|
||||||
func writeJSONResponse(w http.ResponseWriter, body interface{}) {
|
func writeJSONResponse(w http.ResponseWriter, body interface{}) {
|
||||||
bs, err := json.Marshal(body)
|
bs, err := json.Marshal(body)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -17,12 +18,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"encoding/asn1"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
|
jose "gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyType represents the key algo as well as the key size or curve to use.
|
// KeyType represents the key algo as well as the key size or curve to use.
|
||||||
|
@ -118,6 +117,10 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
||||||
defer req.Body.Close()
|
defer req.Body.Close()
|
||||||
|
|
||||||
ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024))
|
ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
|
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -136,9 +139,9 @@ func getKeyAuthorization(token string, key interface{}) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
// Generate the Key Authorization for the challenge
|
||||||
jwk := keyAsJWK(publicKey)
|
jwk := &jose.JSONWebKey{Key: publicKey}
|
||||||
if jwk == nil {
|
if jwk == nil {
|
||||||
return "", errors.New("Could not generate JWK from key.")
|
return "", errors.New("could not generate JWK from key")
|
||||||
}
|
}
|
||||||
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
|
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -146,11 +149,7 @@ func getKeyAuthorization(token string, key interface{}) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// unpad the base64URL
|
// unpad the base64URL
|
||||||
keyThumb := base64.URLEncoding.EncodeToString(thumbBytes)
|
keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
|
||||||
index := strings.Index(keyThumb, "=")
|
|
||||||
if index != -1 {
|
|
||||||
keyThumb = keyThumb[:index]
|
|
||||||
}
|
|
||||||
|
|
||||||
return token + "." + keyThumb, nil
|
return token + "." + keyThumb, nil
|
||||||
}
|
}
|
||||||
|
@ -177,7 +176,7 @@ func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(certificates) == 0 {
|
if len(certificates) == 0 {
|
||||||
return nil, errors.New("No certificates were found while parsing the bundle.")
|
return nil, errors.New("no certificates were found while parsing the bundle")
|
||||||
}
|
}
|
||||||
|
|
||||||
return certificates, nil
|
return certificates, nil
|
||||||
|
@ -192,7 +191,7 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
|
||||||
case "EC PRIVATE KEY":
|
case "EC PRIVATE KEY":
|
||||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("Unknown PEM header value")
|
return nil, errors.New("unknown PEM header value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +210,7 @@ func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
|
||||||
return rsa.GenerateKey(rand.Reader, 8192)
|
return rsa.GenerateKey(rand.Reader, 8192)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("Invalid KeyType: %s", keyType)
|
return nil, fmt.Errorf("invalid KeyType: %s", keyType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
|
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
|
||||||
|
@ -243,10 +242,8 @@ func pemEncode(data interface{}) []byte {
|
||||||
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
||||||
case *rsa.PrivateKey:
|
case *rsa.PrivateKey:
|
||||||
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||||
break
|
|
||||||
case *x509.CertificateRequest:
|
case *x509.CertificateRequest:
|
||||||
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
||||||
break
|
|
||||||
case derCertificateBytes:
|
case derCertificateBytes:
|
||||||
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
|
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ func TestGenerateCSR(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("Error generating CSR:", err)
|
t.Error("Error generating CSR:", err)
|
||||||
}
|
}
|
||||||
if csr == nil || len(csr) == 0 {
|
if len(csr) == 0 {
|
||||||
t.Error("Expected CSR with data, but it was nil or length 0")
|
t.Error("Expected CSR with data, but it was nil or length 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type preCheckDNSFunc func(fqdn, value string) (bool, error)
|
type preCheckDNSFunc func(fqdn, value string) (bool, error)
|
||||||
|
@ -29,6 +29,7 @@ var defaultNameservers = []string{
|
||||||
"google-public-dns-b.google.com:53",
|
"google-public-dns-b.google.com:53",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecursiveNameservers are used to pre-check DNS propagations
|
||||||
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
|
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
|
||||||
|
|
||||||
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
|
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
|
||||||
|
@ -57,8 +58,7 @@ func getNameservers(path string, defaults []string) []string {
|
||||||
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
|
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
|
||||||
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
||||||
// base64URL encoding without padding
|
// base64URL encoding without padding
|
||||||
keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
||||||
value = strings.TrimRight(keyAuthSha, "=")
|
|
||||||
ttl = 120
|
ttl = 120
|
||||||
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
|
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
|
||||||
return
|
return
|
||||||
|
@ -72,7 +72,7 @@ type dnsChallenge struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||||
logf("[INFO][%s] acme: Trying to solve DNS-01", domain)
|
log.Printf("[INFO][%s] acme: Trying to solve DNS-01", domain)
|
||||||
|
|
||||||
if s.provider == nil {
|
if s.provider == nil {
|
||||||
return errors.New("No DNS Provider configured")
|
return errors.New("No DNS Provider configured")
|
||||||
|
@ -97,7 +97,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||||
|
|
||||||
fqdn, value, _ := DNS01Record(domain, keyAuth)
|
fqdn, value, _ := DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
logf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
|
log.Printf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
|
||||||
|
|
||||||
var timeout, interval time.Duration
|
var timeout, interval time.Duration
|
||||||
switch provider := s.provider.(type) {
|
switch provider := s.provider.(type) {
|
||||||
|
@ -114,7 +114,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -28,9 +30,9 @@ func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone)
|
log.Printf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone)
|
||||||
logf("[INFO] acme: %s", dnsRecord)
|
log.Printf("[INFO] acme: %s", dnsRecord)
|
||||||
logf("[INFO] acme: Press 'Enter' when you are done")
|
log.Printf("[INFO] acme: Press 'Enter' when you are done")
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
_, _ = reader.ReadString('\n')
|
_, _ = reader.ReadString('\n')
|
||||||
|
@ -47,7 +49,7 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone)
|
log.Printf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone)
|
||||||
logf("[INFO] acme: %s", dnsRecord)
|
log.Printf("[INFO] acme: %s", dnsRecord)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,9 +100,9 @@ func TestDNSValidServerResponse(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
manualProvider, _ := NewDNSProviderManual()
|
manualProvider, _ := NewDNSProviderManual()
|
||||||
jws := &jws{privKey: privKey, directoryURL: ts.URL}
|
jws := &jws{privKey: privKey, getNonceURL: ts.URL}
|
||||||
solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
|
solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
|
||||||
clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"}
|
clientChallenge := challenge{Type: "dns01", Status: "pending", URL: ts.URL, Token: "http8"}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Second * 2)
|
time.Sleep(time.Second * 2)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -9,8 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tosAgreementError = "Must agree to subscriber agreement before any further actions"
|
tosAgreementError = "Terms of service have changed"
|
||||||
invalidNonceError = "JWS has invalid anti-replay nonce"
|
invalidNonceError = "urn:ietf:params:acme:error:badNonce"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoteError is the base type for all errors specific to the ACME protocol.
|
// RemoteError is the base type for all errors specific to the ACME protocol.
|
||||||
|
@ -42,27 +43,23 @@ type domainError struct {
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
type challengeError struct {
|
// ObtainError is returned when there are specific errors available
|
||||||
RemoteError
|
// per domain. For example in ObtainCertificate
|
||||||
records []validationRecord
|
type ObtainError map[string]error
|
||||||
}
|
|
||||||
|
|
||||||
func (c challengeError) Error() string {
|
func (e ObtainError) Error() string {
|
||||||
|
buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n")
|
||||||
var errStr string
|
for dom, err := range e {
|
||||||
for _, validation := range c.records {
|
buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err))
|
||||||
errStr = errStr + fmt.Sprintf("\tValidation for %s:%s\n\tResolved to:\n\t\t%s\n\tUsed: %s\n\n",
|
|
||||||
validation.Hostname, validation.Port, strings.Join(validation.ResolvedAddresses, "\n\t\t"), validation.UsedAddress)
|
|
||||||
}
|
}
|
||||||
|
return buffer.String()
|
||||||
return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleHTTPError(resp *http.Response) error {
|
func handleHTTPError(resp *http.Response) error {
|
||||||
var errorDetail RemoteError
|
var errorDetail RemoteError
|
||||||
|
|
||||||
contentType := resp.Header.Get("Content-Type")
|
contentType := resp.Header.Get("Content-Type")
|
||||||
if contentType == "application/json" || contentType == "application/problem+json" {
|
if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") {
|
||||||
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
|
err := json.NewDecoder(resp.Body).Decode(&errorDetail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -82,7 +79,7 @@ func handleHTTPError(resp *http.Response) error {
|
||||||
return TOSError{errorDetail}
|
return TOSError{errorDetail}
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) {
|
if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError {
|
||||||
return NonceError{errorDetail}
|
return NonceError{errorDetail}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,5 +87,5 @@ func handleHTTPError(resp *http.Response) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleChallengeError(chlng challenge) error {
|
func handleChallengeError(chlng challenge) error {
|
||||||
return challengeError{chlng.Error, chlng.ValidationRecords}
|
return chlng.Error
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) {
|
||||||
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
|
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
|
||||||
jsonBytes, err := json.Marshal(reqBody)
|
jsonBytes, err := json.Marshal(reqBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Failed to marshal network message...")
|
return nil, errors.New("Failed to marshal network message")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := j.post(uri, jsonBytes)
|
resp, err := j.post(uri, jsonBytes)
|
||||||
|
|
|
@ -2,7 +2,8 @@ package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type httpChallenge struct {
|
type httpChallenge struct {
|
||||||
|
@ -18,7 +19,7 @@ func HTTP01ChallengePath(token string) string {
|
||||||
|
|
||||||
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
|
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
|
log.Printf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
// Generate the Key Authorization for the challenge
|
||||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||||
|
@ -37,5 +38,5 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
|
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
|
||||||
|
@ -61,9 +63,9 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
|
||||||
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
|
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
|
||||||
w.Header().Add("Content-Type", "text/plain")
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
w.Write([]byte(keyAuth))
|
w.Write([]byte(keyAuth))
|
||||||
logf("[INFO][%s] Served key authentication", domain)
|
log.Printf("[INFO][%s] Served key authentication", domain)
|
||||||
} else {
|
} else {
|
||||||
logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method)
|
log.Printf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method)
|
||||||
w.Write([]byte("TEST"))
|
w.Write([]byte("TEST"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
func TestHTTPChallenge(t *testing.T) {
|
func TestHTTPChallenge(t *testing.T) {
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||||
j := &jws{privKey: privKey}
|
j := &jws{privKey: privKey}
|
||||||
clientChallenge := challenge{Type: HTTP01, Token: "http1"}
|
clientChallenge := challenge{Type: string(HTTP01), Token: "http1"}
|
||||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
||||||
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
|
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
|
||||||
resp, err := httpGet(uri)
|
resp, err := httpGet(uri)
|
||||||
|
@ -46,7 +46,7 @@ func TestHTTPChallenge(t *testing.T) {
|
||||||
func TestHTTPChallengeInvalidPort(t *testing.T) {
|
func TestHTTPChallengeInvalidPort(t *testing.T) {
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
||||||
j := &jws{privKey: privKey}
|
j := &jws{privKey: privKey}
|
||||||
clientChallenge := challenge{Type: HTTP01, Token: "http2"}
|
clientChallenge := challenge{Type: string(HTTP01), Token: "http2"}
|
||||||
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
|
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
||||||
|
|
94
acme/jws.go
94
acme/jws.go
|
@ -10,39 +10,29 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v1"
|
"gopkg.in/square/go-jose.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type jws struct {
|
type jws struct {
|
||||||
directoryURL string
|
getNonceURL string
|
||||||
privKey crypto.PrivateKey
|
privKey crypto.PrivateKey
|
||||||
nonces nonceManager
|
kid string
|
||||||
}
|
nonces nonceManager
|
||||||
|
|
||||||
func keyAsJWK(key interface{}) *jose.JsonWebKey {
|
|
||||||
switch k := key.(type) {
|
|
||||||
case *ecdsa.PublicKey:
|
|
||||||
return &jose.JsonWebKey{Key: k, Algorithm: "EC"}
|
|
||||||
case *rsa.PublicKey:
|
|
||||||
return &jose.JsonWebKey{Key: k, Algorithm: "RSA"}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Posts a JWS signed message to the specified URL.
|
// Posts a JWS signed message to the specified URL.
|
||||||
// It does NOT close the response body, so the caller must
|
// It does NOT close the response body, so the caller must
|
||||||
// do that if no error was returned.
|
// do that if no error was returned.
|
||||||
func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
||||||
signedContent, err := j.signContent(content)
|
signedContent, err := j.signContent(url, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
|
data := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
|
||||||
|
resp, err := httpPost(url, "application/jose+json", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
|
return nil, fmt.Errorf("failed to HTTP POST to %s -> %s", url, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce, nonceErr := getNonceFromResponse(resp)
|
nonce, nonceErr := getNonceFromResponse(resp)
|
||||||
|
@ -53,7 +43,7 @@ func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
|
func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) {
|
||||||
|
|
||||||
var alg jose.SignatureAlgorithm
|
var alg jose.SignatureAlgorithm
|
||||||
switch k := j.privKey.(type) {
|
switch k := j.privKey.(type) {
|
||||||
|
@ -67,25 +57,71 @@ func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signer, err := jose.NewSigner(alg, j.privKey)
|
jsonKey := jose.JSONWebKey{
|
||||||
if err != nil {
|
Key: j.privKey,
|
||||||
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
|
KeyID: j.kid,
|
||||||
|
}
|
||||||
|
|
||||||
|
signKey := jose.SigningKey{
|
||||||
|
Algorithm: alg,
|
||||||
|
Key: jsonKey,
|
||||||
|
}
|
||||||
|
options := jose.SignerOptions{
|
||||||
|
NonceSource: j,
|
||||||
|
ExtraHeaders: make(map[jose.HeaderKey]interface{}),
|
||||||
|
}
|
||||||
|
options.ExtraHeaders["url"] = url
|
||||||
|
if j.kid == "" {
|
||||||
|
options.EmbedJWK = true
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(signKey, &options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create jose signer -> %s", err.Error())
|
||||||
}
|
}
|
||||||
signer.SetNonceSource(j)
|
|
||||||
|
|
||||||
signed, err := signer.Sign(content)
|
signed, err := signer.Sign(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
|
||||||
}
|
}
|
||||||
return signed, nil
|
return signed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
|
||||||
|
jwk := jose.JSONWebKey{Key: j.privKey}
|
||||||
|
jwkJSON, err := jwk.Public().MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(
|
||||||
|
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
|
||||||
|
&jose.SignerOptions{
|
||||||
|
EmbedJWK: false,
|
||||||
|
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||||
|
"kid": kid,
|
||||||
|
"url": url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := signer.Sign(jwkJSON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to External Account Binding sign content -> %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return signed, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *jws) Nonce() (string, error) {
|
func (j *jws) Nonce() (string, error) {
|
||||||
if nonce, ok := j.nonces.Pop(); ok {
|
if nonce, ok := j.nonces.Pop(); ok {
|
||||||
return nonce, nil
|
return nonce, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return getNonce(j.directoryURL)
|
return getNonce(j.getNonceURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
type nonceManager struct {
|
type nonceManager struct {
|
||||||
|
@ -115,7 +151,7 @@ func (n *nonceManager) Push(nonce string) {
|
||||||
func getNonce(url string) (string, error) {
|
func getNonce(url string) (string, error) {
|
||||||
resp, err := httpHead(url)
|
resp, err := httpHead(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Failed to get nonce from HTTP HEAD -> %s", err.Error())
|
return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return getNonceFromResponse(resp)
|
return getNonceFromResponse(resp)
|
||||||
|
@ -124,7 +160,7 @@ func getNonce(url string) (string, error) {
|
||||||
func getNonceFromResponse(resp *http.Response) (string, error) {
|
func getNonceFromResponse(resp *http.Response) (string, error) {
|
||||||
nonce := resp.Header.Get("Replay-Nonce")
|
nonce := resp.Header.Get("Replay-Nonce")
|
||||||
if nonce == "" {
|
if nonce == "" {
|
||||||
return "", fmt.Errorf("Server did not respond with a proper nonce header.")
|
return "", fmt.Errorf("server did not respond with a proper nonce header")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nonce, nil
|
return nonce, nil
|
||||||
|
|
120
acme/messages.go
120
acme/messages.go
|
@ -1,59 +1,62 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type directory struct {
|
|
||||||
NewAuthzURL string `json:"new-authz"`
|
|
||||||
NewCertURL string `json:"new-cert"`
|
|
||||||
NewRegURL string `json:"new-reg"`
|
|
||||||
RevokeCertURL string `json:"revoke-cert"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type registrationMessage struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Contact []string `json:"contact"`
|
|
||||||
Delete bool `json:"delete,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registration is returned by the ACME server after the registration
|
|
||||||
// The client implementation should save this registration somewhere.
|
|
||||||
type Registration struct {
|
|
||||||
Resource string `json:"resource,omitempty"`
|
|
||||||
ID int `json:"id"`
|
|
||||||
Key jose.JsonWebKey `json:"key"`
|
|
||||||
Contact []string `json:"contact"`
|
|
||||||
Agreement string `json:"agreement,omitempty"`
|
|
||||||
Authorizations string `json:"authorizations,omitempty"`
|
|
||||||
Certificates string `json:"certificates,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegistrationResource represents all important informations about a registration
|
// RegistrationResource represents all important informations about a registration
|
||||||
// of which the client needs to keep track itself.
|
// of which the client needs to keep track itself.
|
||||||
type RegistrationResource struct {
|
type RegistrationResource struct {
|
||||||
Body Registration `json:"body,omitempty"`
|
Body accountMessage `json:"body,omitempty"`
|
||||||
URI string `json:"uri,omitempty"`
|
URI string `json:"uri,omitempty"`
|
||||||
NewAuthzURL string `json:"new_authzr_uri,omitempty"`
|
|
||||||
TosURL string `json:"terms_of_service,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type authorizationResource struct {
|
type directory struct {
|
||||||
Body authorization
|
NewNonceURL string `json:"newNonce"`
|
||||||
Domain string
|
NewAccountURL string `json:"newAccount"`
|
||||||
NewCertURL string
|
NewOrderURL string `json:"newOrder"`
|
||||||
AuthURL string
|
RevokeCertURL string `json:"revokeCert"`
|
||||||
|
KeyChangeURL string `json:"keyChange"`
|
||||||
|
Meta struct {
|
||||||
|
TermsOfService string `json:"termsOfService"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
CaaIdentities []string `json:"caaIdentities"`
|
||||||
|
ExternalAccountRequired bool `json:"externalAccountRequired"`
|
||||||
|
} `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type accountMessage struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Contact []string `json:"contact,omitempty"`
|
||||||
|
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
|
||||||
|
Orders string `json:"orders,omitempty"`
|
||||||
|
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
|
||||||
|
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderResource struct {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Domains []string `json:"domains,omitempty"`
|
||||||
|
orderMessage `json:"body,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type orderMessage struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Expires string `json:"expires,omitempty"`
|
||||||
|
Identifiers []identifier `json:"identifiers"`
|
||||||
|
NotBefore string `json:"notBefore,omitempty"`
|
||||||
|
NotAfter string `json:"notAfter,omitempty"`
|
||||||
|
Authorizations []string `json:"authorizations,omitempty"`
|
||||||
|
Finalize string `json:"finalize,omitempty"`
|
||||||
|
Certificate string `json:"certificate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type authorization struct {
|
type authorization struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
Status string `json:"status"`
|
||||||
Identifier identifier `json:"identifier"`
|
Expires time.Time `json:"expires"`
|
||||||
Status string `json:"status,omitempty"`
|
Identifier identifier `json:"identifier"`
|
||||||
Expires time.Time `json:"expires,omitempty"`
|
Challenges []challenge `json:"challenges"`
|
||||||
Challenges []challenge `json:"challenges,omitempty"`
|
|
||||||
Combinations [][]int `json:"combinations,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type identifier struct {
|
type identifier struct {
|
||||||
|
@ -61,41 +64,26 @@ type identifier struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type validationRecord struct {
|
|
||||||
URI string `json:"url,omitempty"`
|
|
||||||
Hostname string `json:"hostname,omitempty"`
|
|
||||||
Port string `json:"port,omitempty"`
|
|
||||||
ResolvedAddresses []string `json:"addressesResolved,omitempty"`
|
|
||||||
UsedAddress string `json:"addressUsed,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type challenge struct {
|
type challenge struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
URL string `json:"url"`
|
||||||
Type Challenge `json:"type,omitempty"`
|
Type string `json:"type"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status"`
|
||||||
URI string `json:"uri,omitempty"`
|
Token string `json:"token"`
|
||||||
Token string `json:"token,omitempty"`
|
Validated time.Time `json:"validated"`
|
||||||
KeyAuthorization string `json:"keyAuthorization,omitempty"`
|
KeyAuthorization string `json:"keyAuthorization"`
|
||||||
TLS bool `json:"tls,omitempty"`
|
Error RemoteError `json:"error"`
|
||||||
Iterations int `json:"n,omitempty"`
|
|
||||||
Error RemoteError `json:"error,omitempty"`
|
|
||||||
ValidationRecords []validationRecord `json:"validationRecord,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type csrMessage struct {
|
type csrMessage struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
Csr string `json:"csr"`
|
||||||
Csr string `json:"csr"`
|
|
||||||
Authorizations []string `json:"authorizations"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type revokeCertMessage struct {
|
type revokeCertMessage struct {
|
||||||
Resource string `json:"resource"`
|
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type deactivateAuthMessage struct {
|
type deactivateAuthMessage struct {
|
||||||
Resource string `json:"resource,omitempty"`
|
Status string `jsom:"status"`
|
||||||
Status string `jsom:"status"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertificateResource represents a CA issued certificate.
|
// CertificateResource represents a CA issued certificate.
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
package acme
|
|
|
@ -1,67 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tlsSNIChallenge struct {
|
|
||||||
jws *jws
|
|
||||||
validate validateFunc
|
|
||||||
provider ChallengeProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
|
|
||||||
// FIXME: https://github.com/ietf-wg-acme/acme/pull/22
|
|
||||||
// Currently we implement this challenge to track boulder, not the current spec!
|
|
||||||
|
|
||||||
logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
|
|
||||||
|
|
||||||
// Generate the Key Authorization for the challenge
|
|
||||||
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = t.provider.Present(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[%s] error cleaning up: %v", domain, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
|
|
||||||
func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, string, error) {
|
|
||||||
// generate a new RSA key for the certificates
|
|
||||||
tempPrivKey, err := generatePrivateKey(RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
|
|
||||||
rsaPrivPEM := pemEncode(rsaPrivKey)
|
|
||||||
|
|
||||||
zBytes := sha256.Sum256([]byte(keyAuth))
|
|
||||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
|
||||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
|
||||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificate, domain, nil
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge
|
|
||||||
// It may be instantiated without using the NewTLSProviderServer function if
|
|
||||||
// you want only to use the default values.
|
|
||||||
type TLSProviderServer struct {
|
|
||||||
iface string
|
|
||||||
port string
|
|
||||||
done chan bool
|
|
||||||
listener net.Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTLSProviderServer creates a new TLSProviderServer on the selected interface and port.
|
|
||||||
// Setting iface and / or port to an empty string will make the server fall back to
|
|
||||||
// the "any" interface and port 443 respectively.
|
|
||||||
func NewTLSProviderServer(iface, port string) *TLSProviderServer {
|
|
||||||
return &TLSProviderServer{iface: iface, port: port}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present makes the keyAuth available as a cert
|
|
||||||
func (s *TLSProviderServer) Present(domain, token, keyAuth string) error {
|
|
||||||
if s.port == "" {
|
|
||||||
s.port = "443"
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf := new(tls.Config)
|
|
||||||
tlsConf.Certificates = []tls.Certificate{cert}
|
|
||||||
|
|
||||||
s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.done = make(chan bool)
|
|
||||||
go func() {
|
|
||||||
http.Serve(s.listener, nil)
|
|
||||||
s.done <- true
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanUp closes the HTTP server.
|
|
||||||
func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error {
|
|
||||||
if s.listener == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
s.listener.Close()
|
|
||||||
<-s.done
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTLSSNIChallenge(t *testing.T) {
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
|
||||||
j := &jws{privKey: privKey}
|
|
||||||
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"}
|
|
||||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
|
||||||
conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected to connect to challenge server without an error. %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect the server to only return one certificate
|
|
||||||
connState := conn.ConnectionState()
|
|
||||||
if count := len(connState.PeerCertificates); count != 1 {
|
|
||||||
t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteCert := connState.PeerCertificates[0]
|
|
||||||
if count := len(remoteCert.DNSNames); count != 1 {
|
|
||||||
t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
|
|
||||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
|
||||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
|
||||||
|
|
||||||
if remoteCert.DNSNames[0] != domain {
|
|
||||||
t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}}
|
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
|
|
||||||
t.Errorf("Solve error: got %v, want nil", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTLSSNIChallengeInvalidPort(t *testing.T) {
|
|
||||||
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
|
||||||
j := &jws{privKey: privKey}
|
|
||||||
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"}
|
|
||||||
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}}
|
|
||||||
|
|
||||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
|
||||||
t.Errorf("Solve error: got %v, want error", err)
|
|
||||||
} else if want, want18 := "invalid port 123456", "123456: invalid port"; !strings.HasSuffix(err.Error(), want) && !strings.HasSuffix(err.Error(), want18) {
|
|
||||||
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
|
|
||||||
}
|
|
||||||
}
|
|
33
cli.go
33
cli.go
|
@ -4,26 +4,15 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is used to log errors; if nil, the default log.Logger is used.
|
|
||||||
var Logger *log.Logger
|
|
||||||
|
|
||||||
// logger is an helper function to retrieve the available logger
|
|
||||||
func logger() *log.Logger {
|
|
||||||
if Logger == nil {
|
|
||||||
Logger = log.New(os.Stderr, "", log.LstdFlags)
|
|
||||||
}
|
|
||||||
return Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "dev"
|
version = "dev"
|
||||||
)
|
)
|
||||||
|
@ -45,7 +34,7 @@ func main() {
|
||||||
|
|
||||||
app.Before = func(c *cli.Context) error {
|
app.Before = func(c *cli.Context) error {
|
||||||
if c.GlobalString("path") == "" {
|
if c.GlobalString("path") == "" {
|
||||||
logger().Fatal("Could not determine current working directory. Please pass --path.")
|
log.Fatal("Could not determine current working directory. Please pass --path.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -124,6 +113,18 @@ func main() {
|
||||||
Name: "accept-tos, a",
|
Name: "accept-tos, a",
|
||||||
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
|
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "eab",
|
||||||
|
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "kid",
|
||||||
|
Usage: "Key identifier from External CA. Used for External Account Binding.",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "hmac",
|
||||||
|
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "key-type, k",
|
Name: "key-type, k",
|
||||||
Value: "rsa2048",
|
Value: "rsa2048",
|
||||||
|
@ -136,7 +137,7 @@ func main() {
|
||||||
},
|
},
|
||||||
cli.StringSliceFlag{
|
cli.StringSliceFlag{
|
||||||
Name: "exclude, x",
|
Name: "exclude, x",
|
||||||
Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"tls-sni-01\", \"dns-01\",.",
|
Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"dns-01\".",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "webroot",
|
Name: "webroot",
|
||||||
|
@ -150,10 +151,6 @@ func main() {
|
||||||
Name: "http",
|
Name: "http",
|
||||||
Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port",
|
Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
|
||||||
Name: "tls",
|
|
||||||
Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "dns",
|
Name: "dns",
|
||||||
Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.",
|
Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.",
|
||||||
|
|
213
cli_handlers.go
213
cli_handlers.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -15,6 +16,7 @@ import (
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
"github.com/xenolf/lego/providers/dns"
|
"github.com/xenolf/lego/providers/dns"
|
||||||
"github.com/xenolf/lego/providers/http/memcached"
|
"github.com/xenolf/lego/providers/http/memcached"
|
||||||
"github.com/xenolf/lego/providers/http/webroot"
|
"github.com/xenolf/lego/providers/http/webroot"
|
||||||
|
@ -50,12 +52,12 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
||||||
|
|
||||||
err := checkFolder(c.GlobalString("path"))
|
err := checkFolder(c.GlobalString("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not check/create path: %s", err.Error())
|
log.Fatalf("Could not check/create path: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := NewConfiguration(c)
|
conf := NewConfiguration(c)
|
||||||
if len(c.GlobalString("email")) == 0 {
|
if len(c.GlobalString("email")) == 0 {
|
||||||
logger().Fatal("You have to pass an account (email address) to the program using --email or -m")
|
log.Fatal("You have to pass an account (email address) to the program using --email or -m")
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: move to account struct? Currently MUST pass email.
|
//TODO: move to account struct? Currently MUST pass email.
|
||||||
|
@ -63,12 +65,14 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
||||||
|
|
||||||
keyType, err := conf.KeyType()
|
keyType, err := conf.KeyType()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatal(err.Error())
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
acme.UserAgent = fmt.Sprintf("le-go/cli %s", c.App.Version)
|
||||||
|
|
||||||
client, err := acme.NewClient(c.GlobalString("server"), acc, keyType)
|
client, err := acme.NewClient(c.GlobalString("server"), acc, keyType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not create client: %s", err.Error())
|
log.Fatalf("Could not create client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.GlobalStringSlice("exclude")) > 0 {
|
if len(c.GlobalStringSlice("exclude")) > 0 {
|
||||||
|
@ -78,75 +82,88 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
||||||
if c.GlobalIsSet("webroot") {
|
if c.GlobalIsSet("webroot") {
|
||||||
provider, err := webroot.NewHTTPProvider(c.GlobalString("webroot"))
|
provider, err := webroot.NewHTTPProvider(c.GlobalString("webroot"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.SetChallengeProvider(acme.HTTP01, provider)
|
err = client.SetChallengeProvider(acme.HTTP01, provider)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// --webroot=foo indicates that the user specifically want to do a HTTP challenge
|
// --webroot=foo indicates that the user specifically want to do a HTTP challenge
|
||||||
// infer that the user also wants to exclude all other challenges
|
// infer that the user also wants to exclude all other challenges
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01})
|
client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
|
||||||
}
|
}
|
||||||
if c.GlobalIsSet("memcached-host") {
|
if c.GlobalIsSet("memcached-host") {
|
||||||
provider, err := memcached.NewMemcachedProvider(c.GlobalStringSlice("memcached-host"))
|
provider, err := memcached.NewMemcachedProvider(c.GlobalStringSlice("memcached-host"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.SetChallengeProvider(acme.HTTP01, provider)
|
err = client.SetChallengeProvider(acme.HTTP01, provider)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// --memcached-host=foo:11211 indicates that the user specifically want to do a HTTP challenge
|
// --memcached-host=foo:11211 indicates that the user specifically want to do a HTTP challenge
|
||||||
// infer that the user also wants to exclude all other challenges
|
// infer that the user also wants to exclude all other challenges
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01})
|
client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
|
||||||
}
|
}
|
||||||
if c.GlobalIsSet("http") {
|
if c.GlobalIsSet("http") {
|
||||||
if strings.Index(c.GlobalString("http"), ":") == -1 {
|
if !strings.Contains(c.GlobalString("http"), ":") {
|
||||||
logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.")
|
log.Fatalf("The --http switch only accepts interface:port or :port for its argument.")
|
||||||
}
|
}
|
||||||
client.SetHTTPAddress(c.GlobalString("http"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.GlobalIsSet("tls") {
|
err = client.SetHTTPAddress(c.GlobalString("http"))
|
||||||
if strings.Index(c.GlobalString("tls"), ":") == -1 {
|
if err != nil {
|
||||||
logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.")
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
client.SetTLSAddress(c.GlobalString("tls"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.GlobalIsSet("dns") {
|
if c.GlobalIsSet("dns") {
|
||||||
provider, err := dns.NewDNSChallengeProviderByName(c.GlobalString("dns"))
|
provider, err := dns.NewDNSChallengeProviderByName(c.GlobalString("dns"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.SetChallengeProvider(acme.DNS01, provider)
|
err = client.SetChallengeProvider(acme.DNS01, provider)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// --dns=foo indicates that the user specifically want to do a DNS challenge
|
// --dns=foo indicates that the user specifically want to do a DNS challenge
|
||||||
// infer that the user also wants to exclude all other challenges
|
// infer that the user also wants to exclude all other challenges
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01})
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.GetExternalAccountRequired() && !c.GlobalIsSet("eab") {
|
||||||
|
log.Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return conf, acc, client
|
return conf, acc, client
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCertRes(certRes acme.CertificateResource, conf *Configuration) {
|
func saveCertRes(certRes *acme.CertificateResource, conf *Configuration) {
|
||||||
|
// make sure no funny chars are in the cert names (like wildcards ;))
|
||||||
|
domainName := strings.Replace(certRes.Domain, "*", "_", -1)
|
||||||
|
|
||||||
// We store the certificate, private key and metadata in different files
|
// We store the certificate, private key and metadata in different files
|
||||||
// as web servers would not be able to work with a combined file.
|
// as web servers would not be able to work with a combined file.
|
||||||
certOut := path.Join(conf.CertPath(), certRes.Domain+".crt")
|
certOut := path.Join(conf.CertPath(), domainName+".crt")
|
||||||
privOut := path.Join(conf.CertPath(), certRes.Domain+".key")
|
privOut := path.Join(conf.CertPath(), domainName+".key")
|
||||||
pemOut := path.Join(conf.CertPath(), certRes.Domain+".pem")
|
pemOut := path.Join(conf.CertPath(), domainName+".pem")
|
||||||
metaOut := path.Join(conf.CertPath(), certRes.Domain+".json")
|
metaOut := path.Join(conf.CertPath(), domainName+".json")
|
||||||
issuerOut := path.Join(conf.CertPath(), certRes.Domain+".issuer.crt")
|
issuerOut := path.Join(conf.CertPath(), domainName+".issuer.crt")
|
||||||
|
|
||||||
err := ioutil.WriteFile(certOut, certRes.Certificate, 0600)
|
err := ioutil.WriteFile(certOut, certRes.Certificate, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Unable to save Certificate for domain %s\n\t%s", certRes.Domain, err.Error())
|
log.Fatalf("Unable to save Certificate for domain %s\n\t%v", certRes.Domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if certRes.IssuerCertificate != nil {
|
if certRes.IssuerCertificate != nil {
|
||||||
err = ioutil.WriteFile(issuerOut, certRes.IssuerCertificate, 0600)
|
err = ioutil.WriteFile(issuerOut, certRes.IssuerCertificate, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Unable to save IssuerCertificate for domain %s\n\t%s", certRes.Domain, err.Error())
|
log.Fatalf("Unable to save IssuerCertificate for domain %s\n\t%v", certRes.Domain, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,70 +171,59 @@ func saveCertRes(certRes acme.CertificateResource, conf *Configuration) {
|
||||||
// if we were given a CSR, we don't know the private key
|
// if we were given a CSR, we don't know the private key
|
||||||
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
|
err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Unable to save PrivateKey for domain %s\n\t%s", certRes.Domain, err.Error())
|
log.Fatalf("Unable to save PrivateKey for domain %s\n\t%v", certRes.Domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.context.GlobalBool("pem") {
|
if conf.context.GlobalBool("pem") {
|
||||||
err = ioutil.WriteFile(pemOut, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil), 0600)
|
err = ioutil.WriteFile(pemOut, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil), 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%s", certRes.Domain, err.Error())
|
log.Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", certRes.Domain, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if conf.context.GlobalBool("pem") {
|
} else if conf.context.GlobalBool("pem") {
|
||||||
// we don't have the private key; can't write the .pem file
|
// we don't have the private key; can't write the .pem file
|
||||||
logger().Fatalf("Unable to save pem without private key for domain %s\n\t%s; are you using a CSR?", certRes.Domain, err.Error())
|
log.Fatalf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", certRes.Domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
|
jsonBytes, err := json.MarshalIndent(certRes, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Unable to marshal CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
|
log.Fatalf("Unable to marshal CertResource for domain %s\n\t%v", certRes.Domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(metaOut, jsonBytes, 0600)
|
err = ioutil.WriteFile(metaOut, jsonBytes, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Unable to save CertResource for domain %s\n\t%s", certRes.Domain, err.Error())
|
log.Fatalf("Unable to save CertResource for domain %s\n\t%v", certRes.Domain, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTOS(c *cli.Context, client *acme.Client, acc *Account) {
|
func handleTOS(c *cli.Context, client *acme.Client) bool {
|
||||||
// Check for a global accept override
|
// Check for a global accept override
|
||||||
if c.GlobalBool("accept-tos") {
|
if c.GlobalBool("accept-tos") {
|
||||||
err := client.AgreeToTOS()
|
return true
|
||||||
if err != nil {
|
|
||||||
logger().Fatalf("Could not agree to TOS: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
acc.Save()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
logger().Printf("Please review the TOS at %s", acc.Registration.TosURL)
|
log.Printf("Please review the TOS at %s", client.GetToSURL())
|
||||||
|
|
||||||
for {
|
for {
|
||||||
logger().Println("Do you accept the TOS? Y/n")
|
log.Println("Do you accept the TOS? Y/n")
|
||||||
text, err := reader.ReadString('\n')
|
text, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not read from console: %s", err.Error())
|
log.Fatalf("Could not read from console: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
text = strings.Trim(text, "\r\n")
|
text = strings.Trim(text, "\r\n")
|
||||||
|
|
||||||
if text == "n" {
|
if text == "n" {
|
||||||
logger().Fatal("You did not accept the TOS. Unable to proceed.")
|
log.Fatal("You did not accept the TOS. Unable to proceed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if text == "Y" || text == "y" || text == "" {
|
if text == "Y" || text == "y" || text == "" {
|
||||||
err = client.AgreeToTOS()
|
return true
|
||||||
if err != nil {
|
|
||||||
logger().Fatalf("Could not agree to TOS: %s", err.Error())
|
|
||||||
}
|
|
||||||
acc.Save()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.")
|
log.Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,18 +259,43 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(c *cli.Context) error {
|
func run(c *cli.Context) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
conf, acc, client := setup(c)
|
conf, acc, client := setup(c)
|
||||||
if acc.Registration == nil {
|
if acc.Registration == nil {
|
||||||
reg, err := client.Register()
|
accepted := handleTOS(c, client)
|
||||||
|
if !accepted {
|
||||||
|
log.Fatal("You did not accept the TOS. Unable to proceed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var reg *acme.RegistrationResource
|
||||||
|
|
||||||
|
if c.GlobalBool("eab") {
|
||||||
|
kid := c.GlobalString("kid")
|
||||||
|
hmacEncoded := c.GlobalString("hmac")
|
||||||
|
|
||||||
|
if kid == "" || hmacEncoded == "" {
|
||||||
|
log.Fatalf("Requires arguments --kid and --hmac.")
|
||||||
|
}
|
||||||
|
|
||||||
|
reg, err = client.RegisterWithExternalAccountBinding(
|
||||||
|
accepted,
|
||||||
|
kid,
|
||||||
|
hmacEncoded,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
reg, err = client.Register(accepted)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not complete registration\n\t%s", err.Error())
|
log.Fatalf("Could not complete registration\n\t%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.Registration = reg
|
acc.Registration = reg
|
||||||
acc.Save()
|
acc.Save()
|
||||||
|
|
||||||
logger().Print("!!!! HEADS UP !!!!")
|
log.Print("!!!! HEADS UP !!!!")
|
||||||
logger().Printf(`
|
log.Printf(`
|
||||||
Your account credentials have been saved in your Let's Encrypt
|
Your account credentials have been saved in your Let's Encrypt
|
||||||
configuration directory at "%s".
|
configuration directory at "%s".
|
||||||
You should make a secure backup of this folder now. This
|
You should make a secure backup of this folder now. This
|
||||||
|
@ -274,43 +305,32 @@ func run(c *cli.Context) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the agreement URL is empty, the account still needs to accept the LE TOS.
|
|
||||||
if acc.Registration.Body.Agreement == "" {
|
|
||||||
handleTOS(c, client, acc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we require either domains or csr, but not both
|
// we require either domains or csr, but not both
|
||||||
hasDomains := len(c.GlobalStringSlice("domains")) > 0
|
hasDomains := len(c.GlobalStringSlice("domains")) > 0
|
||||||
hasCsr := len(c.GlobalString("csr")) > 0
|
hasCsr := len(c.GlobalString("csr")) > 0
|
||||||
if hasDomains && hasCsr {
|
if hasDomains && hasCsr {
|
||||||
logger().Fatal("Please specify either --domains/-d or --csr/-c, but not both")
|
log.Fatal("Please specify either --domains/-d or --csr/-c, but not both")
|
||||||
}
|
}
|
||||||
if !hasDomains && !hasCsr {
|
if !hasDomains && !hasCsr {
|
||||||
logger().Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
|
log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
|
||||||
}
|
}
|
||||||
|
|
||||||
var cert acme.CertificateResource
|
var cert *acme.CertificateResource
|
||||||
var failures map[string]error
|
|
||||||
|
|
||||||
if hasDomains {
|
if hasDomains {
|
||||||
// obtain a certificate, generating a new private key
|
// obtain a certificate, generating a new private key
|
||||||
cert, failures = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil, c.Bool("must-staple"))
|
cert, err = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil, c.Bool("must-staple"))
|
||||||
} else {
|
} else {
|
||||||
// read the CSR
|
// read the CSR
|
||||||
csr, err := readCSRFile(c.GlobalString("csr"))
|
csr, err := readCSRFile(c.GlobalString("csr"))
|
||||||
if err != nil {
|
if err == nil {
|
||||||
// we couldn't read the CSR
|
|
||||||
failures = map[string]error{"csr": err}
|
|
||||||
} else {
|
|
||||||
// obtain a certificate for this CSR
|
// obtain a certificate for this CSR
|
||||||
cert, failures = client.ObtainCertificateForCSR(*csr, !c.Bool("no-bundle"))
|
cert, err = client.ObtainCertificateForCSR(*csr, !c.Bool("no-bundle"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(failures) > 0 {
|
if err != nil {
|
||||||
for k, v := range failures {
|
log.Printf("Could not obtain certificates\n\t%v", err)
|
||||||
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure to return a non-zero exit code if ObtainSANCertificate
|
// Make sure to return a non-zero exit code if ObtainSANCertificate
|
||||||
// returned at least one error. Due to us not returning partial
|
// returned at least one error. Due to us not returning partial
|
||||||
|
@ -318,9 +338,8 @@ func run(c *cli.Context) error {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := checkFolder(conf.CertPath())
|
if err = checkFolder(conf.CertPath()); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("Could not check/create path: %v", err)
|
||||||
logger().Fatalf("Could not check/create path: %s", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCertRes(cert, conf)
|
saveCertRes(cert, conf)
|
||||||
|
@ -331,25 +350,27 @@ func run(c *cli.Context) error {
|
||||||
func revoke(c *cli.Context) error {
|
func revoke(c *cli.Context) error {
|
||||||
conf, acc, client := setup(c)
|
conf, acc, client := setup(c)
|
||||||
if acc.Registration == nil {
|
if acc.Registration == nil {
|
||||||
logger().Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
|
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := checkFolder(conf.CertPath())
|
if err := checkFolder(conf.CertPath()); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("Could not check/create path: %v", err)
|
||||||
logger().Fatalf("Could not check/create path: %s", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range c.GlobalStringSlice("domains") {
|
for _, domain := range c.GlobalStringSlice("domains") {
|
||||||
logger().Printf("Trying to revoke certificate for domain %s", domain)
|
log.Printf("Trying to revoke certificate for domain %s", domain)
|
||||||
|
|
||||||
certPath := path.Join(conf.CertPath(), domain+".crt")
|
certPath := path.Join(conf.CertPath(), domain+".crt")
|
||||||
certBytes, err := ioutil.ReadFile(certPath)
|
certBytes, err := ioutil.ReadFile(certPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
err = client.RevokeCertificate(certBytes)
|
err = client.RevokeCertificate(certBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Error while revoking the certificate for domain %s\n\t%s", domain, err.Error())
|
log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err)
|
||||||
} else {
|
} else {
|
||||||
logger().Print("Certificate was revoked.")
|
log.Println("Certificate was revoked.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,14 +380,15 @@ func revoke(c *cli.Context) error {
|
||||||
func renew(c *cli.Context) error {
|
func renew(c *cli.Context) error {
|
||||||
conf, acc, client := setup(c)
|
conf, acc, client := setup(c)
|
||||||
if acc.Registration == nil {
|
if acc.Registration == nil {
|
||||||
logger().Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
|
log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.GlobalStringSlice("domains")) <= 0 {
|
if len(c.GlobalStringSlice("domains")) <= 0 {
|
||||||
logger().Fatal("Please specify at least one domain.")
|
log.Fatal("Please specify at least one domain.")
|
||||||
}
|
}
|
||||||
|
|
||||||
domain := c.GlobalStringSlice("domains")[0]
|
domain := c.GlobalStringSlice("domains")[0]
|
||||||
|
domain = strings.Replace(domain, "*", "_", -1)
|
||||||
|
|
||||||
// load the cert resource from files.
|
// load the cert resource from files.
|
||||||
// We store the certificate, private key and metadata in different files
|
// We store the certificate, private key and metadata in different files
|
||||||
|
@ -377,13 +399,13 @@ func renew(c *cli.Context) error {
|
||||||
|
|
||||||
certBytes, err := ioutil.ReadFile(certPath)
|
certBytes, err := ioutil.ReadFile(certPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Error while loading the certificate for domain %s\n\t%s", domain, err.Error())
|
log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.IsSet("days") {
|
if c.IsSet("days") {
|
||||||
expTime, err := acme.GetPEMCertExpiration(certBytes)
|
expTime, err := acme.GetPEMCertExpiration(certBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Printf("Could not get Certification expiration for domain %s", domain)
|
log.Printf("Could not get Certification expiration for domain %s", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
if int(expTime.Sub(time.Now()).Hours()/24.0) > c.Int("days") {
|
if int(expTime.Sub(time.Now()).Hours()/24.0) > c.Int("days") {
|
||||||
|
@ -393,19 +415,18 @@ func renew(c *cli.Context) error {
|
||||||
|
|
||||||
metaBytes, err := ioutil.ReadFile(metaPath)
|
metaBytes, err := ioutil.ReadFile(metaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Error while loading the meta data for domain %s\n\t%s", domain, err.Error())
|
log.Fatalf("Error while loading the meta data for domain %s\n\t%v", domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var certRes acme.CertificateResource
|
var certRes acme.CertificateResource
|
||||||
err = json.Unmarshal(metaBytes, &certRes)
|
if err := json.Unmarshal(metaBytes, &certRes); err != nil {
|
||||||
if err != nil {
|
log.Fatalf("Error while marshalling the meta data for domain %s\n\t%v", domain, err)
|
||||||
logger().Fatalf("Error while marshalling the meta data for domain %s\n\t%s", domain, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bool("reuse-key") {
|
if c.Bool("reuse-key") {
|
||||||
keyBytes, err := ioutil.ReadFile(privPath)
|
keyBytes, err := ioutil.ReadFile(privPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Error while loading the private key for domain %s\n\t%s", domain, err.Error())
|
log.Fatalf("Error while loading the private key for domain %s\n\t%v", domain, err)
|
||||||
}
|
}
|
||||||
certRes.PrivateKey = keyBytes
|
certRes.PrivateKey = keyBytes
|
||||||
}
|
}
|
||||||
|
@ -414,7 +435,7 @@ func renew(c *cli.Context) error {
|
||||||
|
|
||||||
newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"), c.Bool("must-staple"))
|
newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"), c.Bool("must-staple"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("%s", err.Error())
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCertRes(newCert, conf)
|
saveCertRes(newCert, conf)
|
||||||
|
|
|
@ -52,5 +52,5 @@ func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
||||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("Unknown private key type.")
|
return nil, errors.New("unknown private key type")
|
||||||
}
|
}
|
||||||
|
|
59
log/logger.go
Normal file
59
log/logger.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is an optional custom logger.
|
||||||
|
var Logger *log.Logger
|
||||||
|
|
||||||
|
// Fatal writes a log entry.
|
||||||
|
// It uses Logger if not nil, otherwise it uses the default log.Logger.
|
||||||
|
func Fatal(args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
Logger = log.New(os.Stderr, "", log.LstdFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Fatal(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf writes a log entry.
|
||||||
|
// It uses Logger if not nil, otherwise it uses the default log.Logger.
|
||||||
|
func Fatalf(format string, args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
Logger = log.New(os.Stderr, "", log.LstdFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Fatalf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print writes a log entry.
|
||||||
|
// It uses Logger if not nil, otherwise it uses the default log.Logger.
|
||||||
|
func Print(args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
Logger = log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Print(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println writes a log entry.
|
||||||
|
// It uses Logger if not nil, otherwise it uses the default log.Logger.
|
||||||
|
func Println(args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
Logger = log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Println(args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf writes a log entry.
|
||||||
|
// It uses Logger if not nil, otherwise it uses the default log.Logger.
|
||||||
|
func Printf(format string, args ...interface{}) {
|
||||||
|
if Logger == nil {
|
||||||
|
Logger = log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Printf(format, args...)
|
||||||
|
}
|
|
@ -2,12 +2,13 @@ package auroradns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/edeckers/auroradnsclient"
|
"github.com/edeckers/auroradnsclient"
|
||||||
"github.com/edeckers/auroradnsclient/records"
|
"github.com/edeckers/auroradnsclient/records"
|
||||||
"github.com/edeckers/auroradnsclient/zones"
|
"github.com/edeckers/auroradnsclient/zones"
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNSProvider describes a provider for AuroraDNS
|
// DNSProvider describes a provider for AuroraDNS
|
||||||
|
@ -59,7 +60,7 @@ func (provider *DNSProvider) getZoneInformationByName(name string) (zones.ZoneRe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return zones.ZoneRecord{}, fmt.Errorf("Could not find Zone record")
|
return zones.ZoneRecord{}, fmt.Errorf("could not find Zone record")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present creates a record with a secret
|
// Present creates a record with a secret
|
||||||
|
@ -83,6 +84,9 @@ func (provider *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
authZone = acme.UnFqdn(authZone)
|
authZone = acme.UnFqdn(authZone)
|
||||||
|
|
||||||
zoneRecord, err := provider.getZoneInformationByName(authZone)
|
zoneRecord, err := provider.getZoneInformationByName(authZone)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
reqData :=
|
reqData :=
|
||||||
records.CreateRecordRequest{
|
records.CreateRecordRequest{
|
||||||
|
@ -94,7 +98,7 @@ func (provider *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
|
||||||
respData, err := provider.client.CreateRecord(zoneRecord.ID, reqData)
|
respData, err := provider.client.CreateRecord(zoneRecord.ID, reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Could not create record: '%s'.", err)
|
return fmt.Errorf("could not create record: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.recordIDsMu.Lock()
|
provider.recordIDsMu.Lock()
|
||||||
|
@ -113,12 +117,12 @@ func (provider *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
provider.recordIDsMu.Unlock()
|
provider.recordIDsMu.Unlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Unknown recordID for '%s'", fqdn)
|
return fmt.Errorf("unknown recordID for %q", fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
||||||
if err != nil {
|
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: %q. %v", domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
authZone = acme.UnFqdn(authZone)
|
authZone = acme.UnFqdn(authZone)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fakeAuroraDNSUserId = "asdf1234"
|
var fakeAuroraDNSUserID = "asdf1234"
|
||||||
var fakeAuroraDNSKey = "key"
|
var fakeAuroraDNSKey = "key"
|
||||||
|
|
||||||
func TestAuroraDNSPresent(t *testing.T) {
|
func TestAuroraDNSPresent(t *testing.T) {
|
||||||
|
@ -60,7 +60,7 @@ func TestAuroraDNSPresent(t *testing.T) {
|
||||||
|
|
||||||
defer mock.Close()
|
defer mock.Close()
|
||||||
|
|
||||||
auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserId, fakeAuroraDNSKey)
|
auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserID, fakeAuroraDNSKey)
|
||||||
if auroraProvider == nil {
|
if auroraProvider == nil {
|
||||||
t.Fatal("Expected non-nil AuroraDNS provider, but was nil")
|
t.Fatal("Expected non-nil AuroraDNS provider, but was nil")
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ func TestAuroraDNSCleanUp(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
defer mock.Close()
|
defer mock.Close()
|
||||||
|
|
||||||
auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserId, fakeAuroraDNSKey)
|
auroraProvider, err := NewDNSProviderCredentials(mock.URL, fakeAuroraDNSUserID, fakeAuroraDNSKey)
|
||||||
if auroraProvider == nil {
|
if auroraProvider == nil {
|
||||||
t.Fatal("Expected non-nil AuroraDNS provider, but was nil")
|
t.Fatal("Expected non-nil AuroraDNS provider, but was nil")
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns"
|
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns"
|
||||||
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
"github.com/Azure/go-autorest/autorest"
|
||||||
"github.com/Azure/go-autorest/autorest/adal"
|
"github.com/Azure/go-autorest/autorest/adal"
|
||||||
"github.com/Azure/go-autorest/autorest/azure"
|
"github.com/Azure/go-autorest/autorest/azure"
|
||||||
|
@ -22,32 +20,31 @@ import (
|
||||||
|
|
||||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
||||||
type DNSProvider struct {
|
type DNSProvider struct {
|
||||||
clientId string
|
clientID string
|
||||||
clientSecret string
|
clientSecret string
|
||||||
subscriptionId string
|
subscriptionID string
|
||||||
tenantId string
|
tenantID string
|
||||||
resourceGroup string
|
resourceGroup string
|
||||||
|
context context.Context
|
||||||
context context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProvider returns a DNSProvider instance configured for azure.
|
// NewDNSProvider returns a DNSProvider instance configured for azure.
|
||||||
// Credentials must be passed in the environment variables: AZURE_CLIENT_ID,
|
// Credentials must be passed in the environment variables: AZURE_CLIENT_ID,
|
||||||
// AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP
|
// AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_RESOURCE_GROUP
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
clientId := os.Getenv("AZURE_CLIENT_ID")
|
clientID := os.Getenv("AZURE_CLIENT_ID")
|
||||||
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
|
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
|
||||||
subscriptionId := os.Getenv("AZURE_SUBSCRIPTION_ID")
|
subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID")
|
||||||
tenantId := os.Getenv("AZURE_TENANT_ID")
|
tenantID := os.Getenv("AZURE_TENANT_ID")
|
||||||
resourceGroup := os.Getenv("AZURE_RESOURCE_GROUP")
|
resourceGroup := os.Getenv("AZURE_RESOURCE_GROUP")
|
||||||
return NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup)
|
return NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProviderCredentials uses the supplied credentials to return a
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
// DNSProvider instance configured for azure.
|
// DNSProvider instance configured for azure.
|
||||||
func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId, resourceGroup string) (*DNSProvider, error) {
|
func NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroup string) (*DNSProvider, error) {
|
||||||
if clientId == "" || clientSecret == "" || subscriptionId == "" || tenantId == "" || resourceGroup == "" {
|
if clientID == "" || clientSecret == "" || subscriptionID == "" || tenantID == "" || resourceGroup == "" {
|
||||||
missingEnvVars := []string{}
|
var missingEnvVars []string
|
||||||
for _, envVar := range []string{"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_RESOURCE_GROUP"} {
|
for _, envVar := range []string{"AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_RESOURCE_GROUP"} {
|
||||||
if os.Getenv(envVar) == "" {
|
if os.Getenv(envVar) == "" {
|
||||||
missingEnvVars = append(missingEnvVars, envVar)
|
missingEnvVars = append(missingEnvVars, envVar)
|
||||||
|
@ -57,10 +54,10 @@ func NewDNSProviderCredentials(clientId, clientSecret, subscriptionId, tenantId,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{
|
return &DNSProvider{
|
||||||
clientId: clientId,
|
clientID: clientID,
|
||||||
clientSecret: clientSecret,
|
clientSecret: clientSecret,
|
||||||
subscriptionId: subscriptionId,
|
subscriptionID: subscriptionID,
|
||||||
tenantId: tenantId,
|
tenantID: tenantID,
|
||||||
resourceGroup: resourceGroup,
|
resourceGroup: resourceGroup,
|
||||||
// TODO: A timeout can be added here for cancellation purposes.
|
// TODO: A timeout can be added here for cancellation purposes.
|
||||||
context: context.Background(),
|
context: context.Background(),
|
||||||
|
@ -81,8 +78,12 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rsc := dns.NewRecordSetsClient(c.subscriptionId)
|
rsc := dns.NewRecordSetsClient(c.subscriptionID)
|
||||||
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
|
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
||||||
|
|
||||||
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
|
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
|
||||||
|
@ -90,16 +91,12 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
Name: &relative,
|
Name: &relative,
|
||||||
RecordSetProperties: &dns.RecordSetProperties{
|
RecordSetProperties: &dns.RecordSetProperties{
|
||||||
TTL: to.Int64Ptr(60),
|
TTL: to.Int64Ptr(60),
|
||||||
TxtRecords: &[]dns.TxtRecord{dns.TxtRecord{Value: &[]string{value}}},
|
TxtRecords: &[]dns.TxtRecord{{Value: &[]string{value}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = rsc.CreateOrUpdate(c.context, c.resourceGroup, zone, relative, dns.TXT, rec, "", "")
|
_, err = rsc.CreateOrUpdate(c.context, c.resourceGroup, zone, relative, dns.TXT, rec, "", "")
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the relative record to the domain
|
// Returns the relative record to the domain
|
||||||
|
@ -117,15 +114,16 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
|
relative := toRelativeRecord(fqdn, acme.ToFqdn(zone))
|
||||||
rsc := dns.NewRecordSetsClient(c.subscriptionId)
|
rsc := dns.NewRecordSetsClient(c.subscriptionID)
|
||||||
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
|
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
|
||||||
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
|
||||||
_, err = rsc.Delete(c.context, c.resourceGroup, zone, relative, dns.TXT, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
rsc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
||||||
|
|
||||||
|
_, err = rsc.Delete(c.context, c.resourceGroup, zone, relative, dns.TXT, "")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that azure has a zone for this domain name.
|
// Checks that azure has a zone for this domain name.
|
||||||
|
@ -137,12 +135,14 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
||||||
|
|
||||||
// Now we want to to Azure and get the zone.
|
// Now we want to to Azure and get the zone.
|
||||||
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
|
spt, err := c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
dc := dns.NewZonesClient(c.subscriptionId)
|
dc := dns.NewZonesClient(c.subscriptionID)
|
||||||
dc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
dc.Authorizer = autorest.NewBearerAuthorizer(spt)
|
||||||
|
|
||||||
zone, err := dc.Get(c.context, c.resourceGroup, acme.UnFqdn(authZone))
|
zone, err := dc.Get(c.context, c.resourceGroup, acme.UnFqdn(authZone))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -154,9 +154,9 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
||||||
// NewServicePrincipalTokenFromCredentials creates a new ServicePrincipalToken using values of the
|
// NewServicePrincipalTokenFromCredentials creates a new ServicePrincipalToken using values of the
|
||||||
// passed credentials map.
|
// passed credentials map.
|
||||||
func (c *DNSProvider) newServicePrincipalTokenFromCredentials(scope string) (*adal.ServicePrincipalToken, error) {
|
func (c *DNSProvider) newServicePrincipalTokenFromCredentials(scope string) (*adal.ServicePrincipalToken, error) {
|
||||||
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, c.tenantId)
|
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, c.tenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
return adal.NewServicePrincipalToken(*oauthConfig, c.clientId, c.clientSecret, scope)
|
return adal.NewServicePrincipalToken(*oauthConfig, c.clientID, c.clientSecret, scope)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,18 +13,19 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
const bluecatUrlTemplate = "%s/Services/REST/v1"
|
const bluecatURLTemplate = "%s/Services/REST/v1"
|
||||||
const configType = "Configuration"
|
const configType = "Configuration"
|
||||||
const viewType = "View"
|
const viewType = "View"
|
||||||
const txtType = "TXTRecord"
|
const txtType = "TXTRecord"
|
||||||
const zoneType = "Zone"
|
const zoneType = "Zone"
|
||||||
|
|
||||||
type entityResponse struct {
|
type entityResponse struct {
|
||||||
Id uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Properties string `json:"properties"`
|
Properties string `json:"properties"`
|
||||||
|
@ -33,7 +34,7 @@ type entityResponse struct {
|
||||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses
|
||||||
// Bluecat's Address Manager REST API to manage TXT records for a domain.
|
// Bluecat's Address Manager REST API to manage TXT records for a domain.
|
||||||
type DNSProvider struct {
|
type DNSProvider struct {
|
||||||
baseUrl string
|
baseURL string
|
||||||
userName string
|
userName string
|
||||||
password string
|
password string
|
||||||
configName string
|
configName string
|
||||||
|
@ -55,7 +56,7 @@ func NewDNSProvider() (*DNSProvider, error) {
|
||||||
password := os.Getenv("BLUECAT_PASSWORD")
|
password := os.Getenv("BLUECAT_PASSWORD")
|
||||||
configName := os.Getenv("BLUECAT_CONFIG_NAME")
|
configName := os.Getenv("BLUECAT_CONFIG_NAME")
|
||||||
dnsView := os.Getenv("BLUECAT_DNS_VIEW")
|
dnsView := os.Getenv("BLUECAT_DNS_VIEW")
|
||||||
httpClient := http.Client{Timeout: time.Duration(30 * time.Second)}
|
httpClient := http.Client{Timeout: 30 * time.Second}
|
||||||
return NewDNSProviderCredentials(server, userName, password, configName, dnsView, httpClient)
|
return NewDNSProviderCredentials(server, userName, password, configName, dnsView, httpClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +68,7 @@ func NewDNSProviderCredentials(server, userName, password, configName, dnsView s
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{
|
return &DNSProvider{
|
||||||
baseUrl: fmt.Sprintf(bluecatUrlTemplate, server),
|
baseURL: fmt.Sprintf(bluecatURLTemplate, server),
|
||||||
userName: userName,
|
userName: userName,
|
||||||
password: password,
|
password: password,
|
||||||
configName: configName,
|
configName: configName,
|
||||||
|
@ -79,7 +80,7 @@ func NewDNSProviderCredentials(server, userName, password, configName, dnsView s
|
||||||
// Send a REST request, using query parameters specified. The Authorization
|
// Send a REST request, using query parameters specified. The Authorization
|
||||||
// header will be set if we have an active auth token
|
// header will be set if we have an active auth token
|
||||||
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) {
|
func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) {
|
||||||
url := fmt.Sprintf("%s/%s", d.baseUrl, resource)
|
url := fmt.Sprintf("%s/%s", d.baseURL, resource)
|
||||||
|
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -159,14 +160,14 @@ func (d *DNSProvider) logout() error {
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return fmt.Errorf("Bluecat API request failed to delete session with HTTP status code %d", resp.StatusCode)
|
return fmt.Errorf("Bluecat API request failed to delete session with HTTP status code %d", resp.StatusCode)
|
||||||
} else {
|
}
|
||||||
authBytes, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
authResp := string(authBytes)
|
|
||||||
|
|
||||||
if !strings.Contains(authResp, "successfully") {
|
authBytes, _ := ioutil.ReadAll(resp.Body)
|
||||||
msg := strings.Trim(authResp, "\"")
|
authResp := string(authBytes)
|
||||||
return fmt.Errorf("Bluecat API request failed to delete session: %s", msg)
|
|
||||||
}
|
if !strings.Contains(authResp, "successfully") {
|
||||||
|
msg := strings.Trim(authResp, "\"")
|
||||||
|
return fmt.Errorf("Bluecat API request failed to delete session: %s", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.token = ""
|
d.token = ""
|
||||||
|
@ -175,7 +176,7 @@ func (d *DNSProvider) logout() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup the entity ID of the configuration named in our properties
|
// Lookup the entity ID of the configuration named in our properties
|
||||||
func (d *DNSProvider) lookupConfId() (uint, error) {
|
func (d *DNSProvider) lookupConfID() (uint, error) {
|
||||||
queryArgs := map[string]string{
|
queryArgs := map[string]string{
|
||||||
"parentId": strconv.Itoa(0),
|
"parentId": strconv.Itoa(0),
|
||||||
"name": d.configName,
|
"name": d.configName,
|
||||||
|
@ -193,18 +194,18 @@ func (d *DNSProvider) lookupConfId() (uint, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return conf.Id, nil
|
return conf.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the DNS view with the given name within
|
// Find the DNS view with the given name within
|
||||||
func (d *DNSProvider) lookupViewId(viewName string) (uint, error) {
|
func (d *DNSProvider) lookupViewID(viewName string) (uint, error) {
|
||||||
confId, err := d.lookupConfId()
|
confID, err := d.lookupConfID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
queryArgs := map[string]string{
|
queryArgs := map[string]string{
|
||||||
"parentId": strconv.FormatUint(uint64(confId), 10),
|
"parentId": strconv.FormatUint(uint64(confID), 10),
|
||||||
"name": d.dnsView,
|
"name": d.dnsView,
|
||||||
"type": viewType,
|
"type": viewType,
|
||||||
}
|
}
|
||||||
|
@ -221,13 +222,13 @@ func (d *DNSProvider) lookupViewId(viewName string) (uint, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return view.Id, nil
|
return view.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the entityId of the parent zone by recursing from the root view
|
// Return the entityId of the parent zone by recursing from the root view
|
||||||
// Also return the simple name of the host
|
// Also return the simple name of the host
|
||||||
func (d *DNSProvider) lookupParentZoneId(viewId uint, fqdn string) (uint, string, error) {
|
func (d *DNSProvider) lookupParentZoneID(viewID uint, fqdn string) (uint, string, error) {
|
||||||
parentViewId := viewId
|
parentViewID := viewID
|
||||||
name := ""
|
name := ""
|
||||||
|
|
||||||
if fqdn != "" {
|
if fqdn != "" {
|
||||||
|
@ -236,25 +237,24 @@ func (d *DNSProvider) lookupParentZoneId(viewId uint, fqdn string) (uint, string
|
||||||
name = zones[0]
|
name = zones[0]
|
||||||
|
|
||||||
for i := last; i > -1; i-- {
|
for i := last; i > -1; i-- {
|
||||||
zoneId, err := d.getZone(parentViewId, zones[i])
|
zoneID, err := d.getZone(parentViewID, zones[i])
|
||||||
if err != nil || zoneId == 0 {
|
if err != nil || zoneID == 0 {
|
||||||
return parentViewId, name, err
|
return parentViewID, name, err
|
||||||
}
|
}
|
||||||
if (i > 0) {
|
if i > 0 {
|
||||||
name = strings.Join(zones[0:i],".")
|
name = strings.Join(zones[0:i], ".")
|
||||||
}
|
}
|
||||||
parentViewId = zoneId
|
parentViewID = zoneID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parentViewId, name, nil
|
return parentViewID, name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the DNS zone with the specified name under the parentId
|
// Get the DNS zone with the specified name under the parentId
|
||||||
func (d *DNSProvider) getZone(parentId uint, name string) (uint, error) {
|
func (d *DNSProvider) getZone(parentID uint, name string) (uint, error) {
|
||||||
|
|
||||||
queryArgs := map[string]string{
|
queryArgs := map[string]string{
|
||||||
"parentId": strconv.FormatUint(uint64(parentId), 10),
|
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": zoneType,
|
"type": zoneType,
|
||||||
}
|
}
|
||||||
|
@ -275,7 +275,7 @@ func (d *DNSProvider) getZone(parentId uint, name string) (uint, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return zone.Id, nil
|
return zone.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present creates a TXT record using the specified parameters
|
// Present creates a TXT record using the specified parameters
|
||||||
|
@ -289,21 +289,24 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
viewId, err := d.lookupViewId(d.dnsView)
|
viewID, err := d.lookupViewID(d.dnsView)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
parentZoneId, name, err := d.lookupParentZoneId(viewId, fqdn)
|
parentZoneID, name, err := d.lookupParentZoneID(viewID, fqdn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
queryArgs := map[string]string{
|
queryArgs := map[string]string{
|
||||||
"parentId": strconv.FormatUint(uint64(parentZoneId), 10),
|
"parentId": strconv.FormatUint(uint64(parentZoneID), 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
body := bluecatEntity{
|
body := bluecatEntity{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: "TXTRecord",
|
Type: "TXTRecord",
|
||||||
Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value),
|
Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", ttl, fqdn, value),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := d.sendRequest("POST", "addEntity", body, queryArgs)
|
resp, err := d.sendRequest("POST", "addEntity", body, queryArgs)
|
||||||
|
@ -321,23 +324,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return fmt.Errorf("Bluecat API addEntity request failed: %s", addTxtResp)
|
return fmt.Errorf("Bluecat API addEntity request failed: %s", addTxtResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.deploy(uint(parentZoneId))
|
err = d.deploy(parentZoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.logout()
|
return d.logout()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy the DNS config for the specified entity to the authoritative servers
|
// Deploy the DNS config for the specified entity to the authoritative servers
|
||||||
func (d *DNSProvider) deploy(entityId uint) error {
|
func (d *DNSProvider) deploy(entityID uint) error {
|
||||||
queryArgs := map[string]string{
|
queryArgs := map[string]string{
|
||||||
"entityId": strconv.FormatUint(uint64(entityId), 10),
|
"entityId": strconv.FormatUint(uint64(entityID), 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := d.sendRequest("POST", "quickDeploy", nil, queryArgs)
|
resp, err := d.sendRequest("POST", "quickDeploy", nil, queryArgs)
|
||||||
|
@ -359,18 +357,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
viewId, err := d.lookupViewId(d.dnsView)
|
viewID, err := d.lookupViewID(d.dnsView)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
parentId, name, err := d.lookupParentZoneId(viewId, fqdn)
|
parentID, name, err := d.lookupParentZoneID(viewID, fqdn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
queryArgs := map[string]string{
|
queryArgs := map[string]string{
|
||||||
"parentId": strconv.FormatUint(uint64(parentId), 10),
|
"parentId": strconv.FormatUint(uint64(parentID), 10),
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": txtType,
|
"type": txtType,
|
||||||
}
|
}
|
||||||
|
@ -387,7 +385,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
queryArgs = map[string]string{
|
queryArgs = map[string]string{
|
||||||
"objectId": strconv.FormatUint(uint64(txtRec.Id), 10),
|
"objectId": strconv.FormatUint(uint64(txtRec.ID), 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = d.sendRequest("DELETE", "delete", nil, queryArgs)
|
resp, err = d.sendRequest("DELETE", "delete", nil, queryArgs)
|
||||||
|
@ -396,23 +394,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
err = d.deploy(parentId)
|
err = d.deploy(parentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.logout()
|
return d.logout()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//JSON body for Bluecat entity requests and responses
|
// JSON body for Bluecat entity requests and responses
|
||||||
type bluecatEntity struct {
|
type bluecatEntity struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Properties string `json:"properties"`
|
Properties string `json:"properties"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -14,7 +15,7 @@ var (
|
||||||
bluecatUserName string
|
bluecatUserName string
|
||||||
bluecatPassword string
|
bluecatPassword string
|
||||||
bluecatConfigName string
|
bluecatConfigName string
|
||||||
bluecatDnsView string
|
bluecatDNSView string
|
||||||
bluecatDomain string
|
bluecatDomain string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,8 +25,13 @@ func init() {
|
||||||
bluecatPassword = os.Getenv("BLUECAT_PASSWORD")
|
bluecatPassword = os.Getenv("BLUECAT_PASSWORD")
|
||||||
bluecatDomain = os.Getenv("BLUECAT_DOMAIN")
|
bluecatDomain = os.Getenv("BLUECAT_DOMAIN")
|
||||||
bluecatConfigName = os.Getenv("BLUECAT_CONFIG_NAME")
|
bluecatConfigName = os.Getenv("BLUECAT_CONFIG_NAME")
|
||||||
bluecatDnsView = os.Getenv("BLUECAT_DNS_VIEW")
|
bluecatDNSView = os.Getenv("BLUECAT_DNS_VIEW")
|
||||||
if len(bluecatServer) > 0 && len(bluecatDomain) > 0 && len(bluecatUserName) > 0 && len(bluecatPassword) > 0 && len(bluecatConfigName) > 0 && len(bluecatDnsView) > 0 {
|
if len(bluecatServer) > 0 &&
|
||||||
|
len(bluecatDomain) > 0 &&
|
||||||
|
len(bluecatUserName) > 0 &&
|
||||||
|
len(bluecatPassword) > 0 &&
|
||||||
|
len(bluecatConfigName) > 0 &&
|
||||||
|
len(bluecatDNSView) > 0 {
|
||||||
bluecatLiveTest = true
|
bluecatLiveTest = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,11 +81,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
|
_, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
@ -98,11 +94,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
|
_, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
||||||
|
@ -162,7 +154,7 @@ func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("No existing record found for %s", fqdn)
|
return nil, fmt.Errorf("no existing record found for %s", fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
|
func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
|
||||||
|
@ -187,7 +179,6 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
|
||||||
|
|
||||||
req.Header.Set("X-Auth-Email", c.authEmail)
|
req.Header.Set("X-Auth-Email", c.authEmail)
|
||||||
req.Header.Set("X-Auth-Key", c.authKey)
|
req.Header.Set("X-Auth-Key", c.authKey)
|
||||||
//req.Header.Set("User-Agent", userAgent())
|
|
||||||
|
|
||||||
client := http.Client{Timeout: 30 * time.Second}
|
client := http.Client{Timeout: 30 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
|
@ -1,214 +1,210 @@
|
||||||
// Package cloudxns implements a DNS provider for solving the DNS-01 challenge
|
// Package cloudxns implements a DNS provider for solving the DNS-01 challenge
|
||||||
// using cloudxns DNS.
|
// using cloudxns DNS.
|
||||||
package cloudxns
|
package cloudxns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cloudXNSBaseURL = "https://www.cloudxns.net/api2/"
|
const cloudXNSBaseURL = "https://www.cloudxns.net/api2/"
|
||||||
|
|
||||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
||||||
type DNSProvider struct {
|
type DNSProvider struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
secretKey string
|
secretKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProvider returns a DNSProvider instance configured for cloudxns.
|
// NewDNSProvider returns a DNSProvider instance configured for cloudxns.
|
||||||
// Credentials must be passed in the environment variables: CLOUDXNS_API_KEY
|
// Credentials must be passed in the environment variables: CLOUDXNS_API_KEY
|
||||||
// and CLOUDXNS_SECRET_KEY.
|
// and CLOUDXNS_SECRET_KEY.
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
apiKey := os.Getenv("CLOUDXNS_API_KEY")
|
apiKey := os.Getenv("CLOUDXNS_API_KEY")
|
||||||
secretKey := os.Getenv("CLOUDXNS_SECRET_KEY")
|
secretKey := os.Getenv("CLOUDXNS_SECRET_KEY")
|
||||||
return NewDNSProviderCredentials(apiKey, secretKey)
|
return NewDNSProviderCredentials(apiKey, secretKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProviderCredentials uses the supplied credentials to return a
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
// DNSProvider instance configured for cloudxns.
|
// DNSProvider instance configured for cloudxns.
|
||||||
func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) {
|
func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) {
|
||||||
if apiKey == "" || secretKey == "" {
|
if apiKey == "" || secretKey == "" {
|
||||||
return nil, fmt.Errorf("CloudXNS credentials missing")
|
return nil, fmt.Errorf("CloudXNS credentials missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{
|
return &DNSProvider{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
secretKey: secretKey,
|
secretKey: secretKey,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present creates a TXT record to fulfil the dns-01 challenge.
|
// Present creates a TXT record to fulfil the dns-01 challenge.
|
||||||
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
zoneID, err := c.getHostedZoneID(fqdn)
|
zoneID, err := c.getHostedZoneID(fqdn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.addTxtRecord(zoneID, fqdn, value, ttl)
|
return c.addTxtRecord(zoneID, fqdn, value, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters.
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
zoneID, err := c.getHostedZoneID(fqdn)
|
zoneID, err := c.getHostedZoneID(fqdn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
recordID, err := c.findTxtRecord(zoneID, fqdn)
|
recordID, err := c.findTxtRecord(zoneID, fqdn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.delTxtRecord(recordID, zoneID)
|
return c.delTxtRecord(recordID, zoneID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
||||||
type Data struct {
|
type Data struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := c.makeRequest("GET", "domain", nil)
|
result, err := c.makeRequest("GET", "domain", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var domains []Data
|
var domains []Data
|
||||||
err = json.Unmarshal(result, &domains)
|
err = json.Unmarshal(result, &domains)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, data := range domains {
|
for _, data := range domains {
|
||||||
if data.Domain == authZone {
|
if data.Domain == authZone {
|
||||||
return data.ID, nil
|
return data.ID, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("Zone %s not found in cloudxns for domain %s", authZone, fqdn)
|
return "", fmt.Errorf("zone %s not found in cloudxns for domain %s", authZone, fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSProvider) findTxtRecord(zoneID, fqdn string) (string, error) {
|
func (c *DNSProvider) findTxtRecord(zoneID, fqdn string) (string, error) {
|
||||||
result, err := c.makeRequest("GET", fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil)
|
result, err := c.makeRequest("GET", fmt.Sprintf("record/%s?host_id=0&offset=0&row_num=2000", zoneID), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var records []cloudXNSRecord
|
var records []cloudXNSRecord
|
||||||
err = json.Unmarshal(result, &records)
|
err = json.Unmarshal(result, &records)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
if record.Host == acme.UnFqdn(fqdn) && record.Type == "TXT" {
|
if record.Host == acme.UnFqdn(fqdn) && record.Type == "TXT" {
|
||||||
return record.RecordID, nil
|
return record.RecordID, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("No existing record found for %s", fqdn)
|
return "", fmt.Errorf("no existing record found for %s", fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSProvider) addTxtRecord(zoneID, fqdn, value string, ttl int) error {
|
func (c *DNSProvider) addTxtRecord(zoneID, fqdn, value string, ttl int) error {
|
||||||
id, err := strconv.Atoi(zoneID)
|
id, err := strconv.Atoi(zoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := cloudXNSRecord{
|
payload := cloudXNSRecord{
|
||||||
ID: id,
|
ID: id,
|
||||||
Host: acme.UnFqdn(fqdn),
|
Host: acme.UnFqdn(fqdn),
|
||||||
Value: value,
|
Value: value,
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
LineID: 1,
|
LineID: 1,
|
||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.makeRequest("POST", "record", body)
|
_, err = c.makeRequest("POST", "record", body)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
func (c *DNSProvider) delTxtRecord(recordID, zoneID string) error {
|
||||||
return nil
|
_, err := c.makeRequest("DELETE", fmt.Sprintf("record/%s/%s", recordID, zoneID), nil)
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
func (c *DNSProvider) delTxtRecord(recordID, zoneID string) error {
|
|
||||||
_, err := c.makeRequest("DELETE", fmt.Sprintf("record/%s/%s", recordID, zoneID), nil)
|
func (c *DNSProvider) hmac(url, date, body string) string {
|
||||||
return err
|
sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey))
|
||||||
}
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
func (c *DNSProvider) hmac(url, date, body string) string {
|
|
||||||
sum := md5.Sum([]byte(c.apiKey + url + body + date + c.secretKey))
|
func (c *DNSProvider) makeRequest(method, uri string, body []byte) (json.RawMessage, error) {
|
||||||
return hex.EncodeToString(sum[:])
|
type APIResponse struct {
|
||||||
}
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
func (c *DNSProvider) makeRequest(method, uri string, body []byte) (json.RawMessage, error) {
|
Data json.RawMessage `json:"data,omitempty"`
|
||||||
type APIResponse struct {
|
}
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
url := cloudXNSBaseURL + uri
|
||||||
Data json.RawMessage `json:"data,omitempty"`
|
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
url := cloudXNSBaseURL + uri
|
}
|
||||||
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
requestDate := time.Now().Format(time.RFC1123Z)
|
||||||
return nil, err
|
|
||||||
}
|
req.Header.Set("API-KEY", c.apiKey)
|
||||||
|
req.Header.Set("API-REQUEST-DATE", requestDate)
|
||||||
requestDate := time.Now().Format(time.RFC1123Z)
|
req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body)))
|
||||||
|
req.Header.Set("API-FORMAT", "json")
|
||||||
req.Header.Set("API-KEY", c.apiKey)
|
|
||||||
req.Header.Set("API-REQUEST-DATE", requestDate)
|
resp, err := acme.HTTPClient.Do(req)
|
||||||
req.Header.Set("API-HMAC", c.hmac(url, requestDate, string(body)))
|
if err != nil {
|
||||||
req.Header.Set("API-FORMAT", "json")
|
return nil, err
|
||||||
|
}
|
||||||
resp, err := acme.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
defer resp.Body.Close()
|
||||||
return nil, err
|
|
||||||
}
|
var r APIResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&r)
|
||||||
defer resp.Body.Close()
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
var r APIResponse
|
}
|
||||||
err = json.NewDecoder(resp.Body).Decode(&r)
|
|
||||||
if err != nil {
|
if r.Code != 1 {
|
||||||
return nil, err
|
return nil, fmt.Errorf("CloudXNS API Error: %s", r.Message)
|
||||||
}
|
}
|
||||||
|
return r.Data, nil
|
||||||
if r.Code != 1 {
|
}
|
||||||
return nil, fmt.Errorf("CloudXNS API Error: %s", r.Message)
|
|
||||||
}
|
type cloudXNSRecord struct {
|
||||||
return r.Data, nil
|
ID int `json:"domain_id,omitempty"`
|
||||||
}
|
RecordID string `json:"record_id,omitempty"`
|
||||||
|
|
||||||
type cloudXNSRecord struct {
|
Host string `json:"host"`
|
||||||
ID int `json:"domain_id,omitempty"`
|
Value string `json:"value"`
|
||||||
RecordID string `json:"record_id,omitempty"`
|
Type string `json:"type"`
|
||||||
|
LineID int `json:"line_id,string"`
|
||||||
Host string `json:"host"`
|
TTL int `json:"ttl,string"`
|
||||||
Value string `json:"value"`
|
}
|
||||||
Type string `json:"type"`
|
|
||||||
LineID int `json:"line_id,string"`
|
|
||||||
TTL int `json:"ttl,string"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,80 +1,80 @@
|
||||||
package cloudxns
|
package cloudxns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cxLiveTest bool
|
cxLiveTest bool
|
||||||
cxAPIKey string
|
cxAPIKey string
|
||||||
cxSecretKey string
|
cxSecretKey string
|
||||||
cxDomain string
|
cxDomain string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cxAPIKey = os.Getenv("CLOUDXNS_API_KEY")
|
cxAPIKey = os.Getenv("CLOUDXNS_API_KEY")
|
||||||
cxSecretKey = os.Getenv("CLOUDXNS_SECRET_KEY")
|
cxSecretKey = os.Getenv("CLOUDXNS_SECRET_KEY")
|
||||||
cxDomain = os.Getenv("CLOUDXNS_DOMAIN")
|
cxDomain = os.Getenv("CLOUDXNS_DOMAIN")
|
||||||
if len(cxAPIKey) > 0 && len(cxSecretKey) > 0 && len(cxDomain) > 0 {
|
if len(cxAPIKey) > 0 && len(cxSecretKey) > 0 && len(cxDomain) > 0 {
|
||||||
cxLiveTest = true
|
cxLiveTest = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreCloudXNSEnv() {
|
func restoreCloudXNSEnv() {
|
||||||
os.Setenv("CLOUDXNS_API_KEY", cxAPIKey)
|
os.Setenv("CLOUDXNS_API_KEY", cxAPIKey)
|
||||||
os.Setenv("CLOUDXNS_SECRET_KEY", cxSecretKey)
|
os.Setenv("CLOUDXNS_SECRET_KEY", cxSecretKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewDNSProviderValid(t *testing.T) {
|
func TestNewDNSProviderValid(t *testing.T) {
|
||||||
os.Setenv("CLOUDXNS_API_KEY", "")
|
os.Setenv("CLOUDXNS_API_KEY", "")
|
||||||
os.Setenv("CLOUDXNS_SECRET_KEY", "")
|
os.Setenv("CLOUDXNS_SECRET_KEY", "")
|
||||||
_, err := NewDNSProviderCredentials("123", "123")
|
_, err := NewDNSProviderCredentials("123", "123")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
restoreCloudXNSEnv()
|
restoreCloudXNSEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewDNSProviderValidEnv(t *testing.T) {
|
func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||||
os.Setenv("CLOUDXNS_API_KEY", "123")
|
os.Setenv("CLOUDXNS_API_KEY", "123")
|
||||||
os.Setenv("CLOUDXNS_SECRET_KEY", "123")
|
os.Setenv("CLOUDXNS_SECRET_KEY", "123")
|
||||||
_, err := NewDNSProvider()
|
_, err := NewDNSProvider()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
restoreCloudXNSEnv()
|
restoreCloudXNSEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||||
os.Setenv("CLOUDXNS_API_KEY", "")
|
os.Setenv("CLOUDXNS_API_KEY", "")
|
||||||
os.Setenv("CLOUDXNS_SECRET_KEY", "")
|
os.Setenv("CLOUDXNS_SECRET_KEY", "")
|
||||||
_, err := NewDNSProvider()
|
_, err := NewDNSProvider()
|
||||||
assert.EqualError(t, err, "CloudXNS credentials missing")
|
assert.EqualError(t, err, "CloudXNS credentials missing")
|
||||||
restoreCloudXNSEnv()
|
restoreCloudXNSEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudXNSPresent(t *testing.T) {
|
func TestCloudXNSPresent(t *testing.T) {
|
||||||
if !cxLiveTest {
|
if !cxLiveTest {
|
||||||
t.Skip("skipping live test")
|
t.Skip("skipping live test")
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey)
|
provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = provider.Present(cxDomain, "", "123d==")
|
err = provider.Present(cxDomain, "", "123d==")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudXNSCleanUp(t *testing.T) {
|
func TestCloudXNSCleanUp(t *testing.T) {
|
||||||
if !cxLiveTest {
|
if !cxLiveTest {
|
||||||
t.Skip("skipping live test")
|
t.Skip("skipping live test")
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Second * 2)
|
time.Sleep(time.Second * 2)
|
||||||
|
|
||||||
provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey)
|
provider, err := NewDNSProviderCredentials(cxAPIKey, cxSecretKey)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = provider.CleanUp(cxDomain, "", "123d==")
|
err = provider.CleanUp(cxDomain, "", "123d==")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,12 @@ type DNSProvider struct {
|
||||||
recordIDsMu sync.Mutex
|
recordIDsMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 60 * time.Second, 5 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
// NewDNSProvider returns a DNSProvider instance configured for Digital
|
// NewDNSProvider returns a DNSProvider instance configured for Digital
|
||||||
// Ocean. Credentials must be passed in the environment variable:
|
// Ocean. Credentials must be passed in the environment variable:
|
||||||
// DO_AUTH_TOKEN.
|
// DO_AUTH_TOKEN.
|
||||||
|
@ -44,34 +50,17 @@ func NewDNSProviderCredentials(apiAuthToken string) (*DNSProvider, error) {
|
||||||
|
|
||||||
// Present creates a TXT record using the specified parameters
|
// Present creates a TXT record using the specified parameters
|
||||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
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 {
|
|
||||||
RecordType string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// txtRecordResponse represents a response from DO's API after making a TXT record
|
|
||||||
type txtRecordResponse struct {
|
|
||||||
DomainRecord struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
} `json:"domain_record"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
||||||
if err != nil {
|
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)
|
authZone = acme.UnFqdn(authZone)
|
||||||
|
|
||||||
reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone)
|
reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone)
|
||||||
reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value}
|
reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value, TTL: 30}
|
||||||
body, err := json.Marshal(reqData)
|
body, err := json.Marshal(reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -124,7 +113,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
|
||||||
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
||||||
if err != nil {
|
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)
|
authZone = acme.UnFqdn(authZone)
|
||||||
|
@ -164,3 +153,21 @@ type digitalOceanAPIError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var digitalOceanBaseURL = "https://api.digitalocean.com"
|
var digitalOceanBaseURL = "https://api.digitalocean.com"
|
||||||
|
|
||||||
|
// txtRecordRequest represents the request body to DO's API to make a TXT record
|
||||||
|
type txtRecordRequest struct {
|
||||||
|
RecordType string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// txtRecordResponse represents a response from DO's API after making a TXT record
|
||||||
|
type txtRecordResponse struct {
|
||||||
|
DomainRecord struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"domain_record"`
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ func TestDigitalOceanPresent(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error reading request body: %v", err)
|
t.Fatalf("Error reading request body: %v", err)
|
||||||
}
|
}
|
||||||
if got, want := string(reqBody), `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"}`; got != want {
|
if got, want := string(reqBody), `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`; got != want {
|
||||||
t.Errorf("Expected body data to be: `%s` but got `%s`", want, got)
|
t.Errorf("Expected body data to be: `%s` but got `%s`", want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
// Factory for DNS providers
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -7,6 +6,7 @@ import (
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
"github.com/xenolf/lego/providers/dns/auroradns"
|
"github.com/xenolf/lego/providers/dns/auroradns"
|
||||||
"github.com/xenolf/lego/providers/dns/azure"
|
"github.com/xenolf/lego/providers/dns/azure"
|
||||||
|
"github.com/xenolf/lego/providers/dns/bluecat"
|
||||||
"github.com/xenolf/lego/providers/dns/cloudflare"
|
"github.com/xenolf/lego/providers/dns/cloudflare"
|
||||||
"github.com/xenolf/lego/providers/dns/cloudxns"
|
"github.com/xenolf/lego/providers/dns/cloudxns"
|
||||||
"github.com/xenolf/lego/providers/dns/digitalocean"
|
"github.com/xenolf/lego/providers/dns/digitalocean"
|
||||||
|
@ -35,9 +35,9 @@ 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/vultr"
|
"github.com/xenolf/lego/providers/dns/vultr"
|
||||||
"github.com/xenolf/lego/providers/dns/bluecat"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewDNSChallengeProviderByName Factory for DNS providers
|
||||||
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
|
func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) {
|
||||||
var err error
|
var err error
|
||||||
var provider acme.ChallengeProvider
|
var provider acme.ChallengeProvider
|
||||||
|
@ -107,7 +107,7 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
||||||
case "exec":
|
case "exec":
|
||||||
provider, err = exec.NewDNSProvider()
|
provider, err = exec.NewDNSProvider()
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("Unrecognised DNS provider: %s", name)
|
err = fmt.Errorf("unrecognised DNS provider: %s", name)
|
||||||
}
|
}
|
||||||
return provider, err
|
return provider, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +23,14 @@ type DNSProvider struct {
|
||||||
// See: https://developer.dnsimple.com/v2/#authentication
|
// See: https://developer.dnsimple.com/v2/#authentication
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
accessToken := os.Getenv("DNSIMPLE_OAUTH_TOKEN")
|
accessToken := os.Getenv("DNSIMPLE_OAUTH_TOKEN")
|
||||||
baseUrl := os.Getenv("DNSIMPLE_BASE_URL")
|
baseURL := os.Getenv("DNSIMPLE_BASE_URL")
|
||||||
|
|
||||||
return NewDNSProviderCredentials(accessToken, baseUrl)
|
return NewDNSProviderCredentials(accessToken, baseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProviderCredentials uses the supplied credentials to return a
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
// DNSProvider instance configured for dnsimple.
|
// DNSProvider instance configured for dnsimple.
|
||||||
func NewDNSProviderCredentials(accessToken, baseUrl string) (*DNSProvider, error) {
|
func NewDNSProviderCredentials(accessToken, baseURL string) (*DNSProvider, error) {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
return nil, fmt.Errorf("DNSimple OAuth token is missing")
|
return nil, fmt.Errorf("DNSimple OAuth token is missing")
|
||||||
}
|
}
|
||||||
|
@ -38,8 +38,8 @@ func NewDNSProviderCredentials(accessToken, baseUrl string) (*DNSProvider, error
|
||||||
client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(accessToken))
|
client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(accessToken))
|
||||||
client.UserAgent = "lego"
|
client.UserAgent = "lego"
|
||||||
|
|
||||||
if baseUrl != "" {
|
if baseURL != "" {
|
||||||
client.BaseURL = baseUrl
|
client.BaseURL = baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{client: client}, nil
|
return &DNSProvider{client: client}, nil
|
||||||
|
@ -119,8 +119,7 @@ func (c *DNSProvider) getHostedZone(domain string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if hostedZone.ID == 0 {
|
if hostedZone.ID == 0 {
|
||||||
return "", fmt.Errorf("Zone %s not found in DNSimple for domain %s", authZone, domain)
|
return "", fmt.Errorf("zone %s not found in DNSimple for domain %s", authZone, domain)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hostedZone.Name, nil
|
return hostedZone.Name, nil
|
||||||
|
@ -173,7 +172,7 @@ func (c *DNSProvider) getAccountID() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if whoamiResponse.Data.Account == nil {
|
if whoamiResponse.Data.Account == nil {
|
||||||
return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token.")
|
return "", fmt.Errorf("DNSimple user tokens are not supported, please use an account token")
|
||||||
}
|
}
|
||||||
|
|
||||||
return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil
|
return strconv.FormatInt(whoamiResponse.Data.Account.ID, 10), nil
|
||||||
|
|
|
@ -12,19 +12,19 @@ var (
|
||||||
dnsimpleLiveTest bool
|
dnsimpleLiveTest bool
|
||||||
dnsimpleOauthToken string
|
dnsimpleOauthToken string
|
||||||
dnsimpleDomain string
|
dnsimpleDomain string
|
||||||
dnsimpleBaseUrl string
|
dnsimpleBaseURL string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
dnsimpleOauthToken = os.Getenv("DNSIMPLE_OAUTH_TOKEN")
|
dnsimpleOauthToken = os.Getenv("DNSIMPLE_OAUTH_TOKEN")
|
||||||
dnsimpleDomain = os.Getenv("DNSIMPLE_DOMAIN")
|
dnsimpleDomain = os.Getenv("DNSIMPLE_DOMAIN")
|
||||||
dnsimpleBaseUrl = "https://api.sandbox.dnsimple.com"
|
dnsimpleBaseURL = "https://api.sandbox.dnsimple.com"
|
||||||
|
|
||||||
if len(dnsimpleOauthToken) > 0 && len(dnsimpleDomain) > 0 {
|
if len(dnsimpleOauthToken) > 0 && len(dnsimpleDomain) > 0 {
|
||||||
baseUrl := os.Getenv("DNSIMPLE_BASE_URL")
|
baseURL := os.Getenv("DNSIMPLE_BASE_URL")
|
||||||
|
|
||||||
if baseUrl != "" {
|
if baseURL != "" {
|
||||||
dnsimpleBaseUrl = baseUrl
|
dnsimpleBaseURL = baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsimpleLiveTest = true
|
dnsimpleLiveTest = true
|
||||||
|
@ -33,7 +33,7 @@ func init() {
|
||||||
|
|
||||||
func restoreDNSimpleEnv() {
|
func restoreDNSimpleEnv() {
|
||||||
os.Setenv("DNSIMPLE_OAUTH_TOKEN", dnsimpleOauthToken)
|
os.Setenv("DNSIMPLE_OAUTH_TOKEN", dnsimpleOauthToken)
|
||||||
os.Setenv("DNSIMPLE_BASE_URL", dnsimpleBaseUrl)
|
os.Setenv("DNSIMPLE_BASE_URL", dnsimpleBaseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -114,7 +114,7 @@ func TestLiveDNSimplePresent(t *testing.T) {
|
||||||
t.Skip("skipping live test")
|
t.Skip("skipping live test")
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseUrl)
|
provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseURL)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = provider.Present(dnsimpleDomain, "", "123d==")
|
err = provider.Present(dnsimpleDomain, "", "123d==")
|
||||||
|
@ -132,7 +132,7 @@ func TestLiveDNSimpleCleanUp(t *testing.T) {
|
||||||
|
|
||||||
time.Sleep(time.Second * 1)
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseUrl)
|
provider, err := NewDNSProviderCredentials(dnsimpleOauthToken, dnsimpleBaseURL)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = provider.CleanUp(dnsimpleDomain, "", "123d==")
|
err = provider.CleanUp(dnsimpleDomain, "", "123d==")
|
||||||
|
|
|
@ -95,11 +95,7 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error {
|
||||||
record := &Record{Type: "TXT", Name: name, Value: value, TTL: ttl}
|
record := &Record{Type: "TXT", Name: name, Value: value, TTL: ttl}
|
||||||
|
|
||||||
err = d.createRecord(domain, record)
|
err = d.createRecord(domain, record)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT records matching the specified parameters
|
// CleanUp removes the TXT records matching the specified parameters
|
||||||
|
@ -226,7 +222,7 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{})
|
||||||
}
|
}
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Timeout: time.Duration(10 * time.Second),
|
Timeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -28,6 +28,7 @@ func TestPresentAndCleanup(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := NewDNSProvider()
|
provider, err := NewDNSProvider()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = provider.Present(testDomain, "", "123d==")
|
err = provider.Present(testDomain, "", "123d==")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -1,82 +1,81 @@
|
||||||
// Adds lego support for http://duckdns.org .
|
// Package duckdns Adds lego support for http://duckdns.org .
|
||||||
//
|
// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.
|
||||||
// See http://www.duckdns.org/spec.jsp for more info on updating TXT records.
|
package duckdns
|
||||||
package duckdns
|
|
||||||
|
import (
|
||||||
import (
|
"errors"
|
||||||
"errors"
|
"fmt"
|
||||||
"fmt"
|
"io/ioutil"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"os"
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
"github.com/xenolf/lego/acme"
|
)
|
||||||
)
|
|
||||||
|
// DNSProvider adds and removes the record for the DNS challenge
|
||||||
// DNSProvider adds and removes the record for the DNS challenge
|
type DNSProvider struct {
|
||||||
type DNSProvider struct {
|
// The duckdns api token
|
||||||
// The duckdns api token
|
token string
|
||||||
token string
|
}
|
||||||
}
|
|
||||||
|
// NewDNSProvider returns a new DNS provider using
|
||||||
// NewDNSProvider returns a new DNS provider using
|
// environment variable DUCKDNS_TOKEN for adding and removing the DNS record.
|
||||||
// environment variable DUCKDNS_TOKEN for adding and removing the DNS record.
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
duckdnsToken := os.Getenv("DUCKDNS_TOKEN")
|
||||||
duckdnsToken := os.Getenv("DUCKDNS_TOKEN")
|
|
||||||
|
return NewDNSProviderCredentials(duckdnsToken)
|
||||||
return NewDNSProviderCredentials(duckdnsToken)
|
}
|
||||||
}
|
|
||||||
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
// NewDNSProviderCredentials uses the supplied credentials to return a
|
// DNSProvider instance configured for http://duckdns.org .
|
||||||
// DNSProvider instance configured for http://duckdns.org .
|
func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) {
|
||||||
func NewDNSProviderCredentials(duckdnsToken string) (*DNSProvider, error) {
|
if duckdnsToken == "" {
|
||||||
if duckdnsToken == "" {
|
return nil, errors.New("environment variable DUCKDNS_TOKEN not set")
|
||||||
return nil, errors.New("environment variable DUCKDNS_TOKEN not set")
|
}
|
||||||
}
|
|
||||||
|
return &DNSProvider{token: duckdnsToken}, nil
|
||||||
return &DNSProvider{token: duckdnsToken}, nil
|
}
|
||||||
}
|
|
||||||
|
// makeDuckdnsURL creates a url to clear the set or unset the TXT record.
|
||||||
// makeDuckdnsURL creates a url to clear the set or unset the TXT record.
|
// txt == "" will clear the TXT record.
|
||||||
// txt == "" will clear the TXT record.
|
func makeDuckdnsURL(domain, token, txt string) string {
|
||||||
func makeDuckdnsURL(domain, token, txt string) string {
|
requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token)
|
||||||
requestBase := fmt.Sprintf("https://www.duckdns.org/update?domains=%s&token=%s", domain, token)
|
if txt == "" {
|
||||||
if txt == "" {
|
return requestBase + "&clear=true"
|
||||||
return requestBase + "&clear=true"
|
}
|
||||||
}
|
return requestBase + "&txt=" + txt
|
||||||
return requestBase + "&txt=" + txt
|
}
|
||||||
}
|
|
||||||
|
func issueDuckdnsRequest(url string) error {
|
||||||
func issueDuckdnsRequest(url string) error {
|
response, err := acme.HTTPClient.Get(url)
|
||||||
response, err := acme.HTTPClient.Get(url)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
defer response.Body.Close()
|
||||||
defer response.Body.Close()
|
|
||||||
|
bodyBytes, err := ioutil.ReadAll(response.Body)
|
||||||
bodyBytes, err := ioutil.ReadAll(response.Body)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
body := string(bodyBytes)
|
||||||
body := string(bodyBytes)
|
if body != "OK" {
|
||||||
if body != "OK" {
|
return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url)
|
||||||
return fmt.Errorf("Request to change TXT record for duckdns returned the following result (%s) this does not match expectation (OK) used url [%s]", body, url)
|
}
|
||||||
}
|
return nil
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
|
// Present creates a TXT record to fulfil the dns-01 challenge.
|
||||||
// Present creates a TXT record to fulfil the dns-01 challenge.
|
// In duckdns you only have one TXT record shared with
|
||||||
// In duckdns you only have one TXT record shared with
|
// the domain and all sub domains.
|
||||||
// the domain and all sub domains.
|
//
|
||||||
//
|
// To update the TXT record we just need to make one simple get request.
|
||||||
// To update the TXT record we just need to make one simple get request.
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
_, txtRecord, _ := acme.DNS01Record(domain, keyAuth)
|
url := makeDuckdnsURL(domain, d.token, txtRecord)
|
||||||
url := makeDuckdnsURL(domain, d.token, txtRecord)
|
return issueDuckdnsRequest(url)
|
||||||
return issueDuckdnsRequest(url)
|
}
|
||||||
}
|
|
||||||
|
// CleanUp clears duckdns TXT record
|
||||||
// CleanUp clears duckdns TXT record
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
url := makeDuckdnsURL(domain, d.token, "")
|
||||||
url := makeDuckdnsURL(domain, d.token, "")
|
return issueDuckdnsRequest(url)
|
||||||
return issueDuckdnsRequest(url)
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,65 +1,65 @@
|
||||||
package duckdns
|
package duckdns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
duckdnsLiveTest bool
|
duckdnsLiveTest bool
|
||||||
duckdnsToken string
|
duckdnsToken string
|
||||||
duckdnsDomain string
|
duckdnsDomain string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
duckdnsToken = os.Getenv("DUCKDNS_TOKEN")
|
duckdnsToken = os.Getenv("DUCKDNS_TOKEN")
|
||||||
duckdnsDomain = os.Getenv("DUCKDNS_DOMAIN")
|
duckdnsDomain = os.Getenv("DUCKDNS_DOMAIN")
|
||||||
if len(duckdnsDomain) > 0 && len(duckdnsDomain) > 0 {
|
if len(duckdnsToken) > 0 && len(duckdnsDomain) > 0 {
|
||||||
duckdnsLiveTest = true
|
duckdnsLiveTest = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreDuckdnsEnv() {
|
func restoreDuckdnsEnv() {
|
||||||
os.Setenv("DUCKDNS_TOKEN", duckdnsToken)
|
os.Setenv("DUCKDNS_TOKEN", duckdnsToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewDNSProviderValidEnv(t *testing.T) {
|
func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||||
os.Setenv("DUCKDNS_TOKEN", "123")
|
os.Setenv("DUCKDNS_TOKEN", "123")
|
||||||
_, err := NewDNSProvider()
|
_, err := NewDNSProvider()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
restoreDuckdnsEnv()
|
restoreDuckdnsEnv()
|
||||||
}
|
}
|
||||||
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||||
os.Setenv("DUCKDNS_TOKEN", "")
|
os.Setenv("DUCKDNS_TOKEN", "")
|
||||||
_, err := NewDNSProvider()
|
_, err := NewDNSProvider()
|
||||||
assert.EqualError(t, err, "environment variable DUCKDNS_TOKEN not set")
|
assert.EqualError(t, err, "environment variable DUCKDNS_TOKEN not set")
|
||||||
restoreDuckdnsEnv()
|
restoreDuckdnsEnv()
|
||||||
}
|
}
|
||||||
func TestLiveDuckdnsPresent(t *testing.T) {
|
func TestLiveDuckdnsPresent(t *testing.T) {
|
||||||
if !duckdnsLiveTest {
|
if !duckdnsLiveTest {
|
||||||
t.Skip("skipping live test")
|
t.Skip("skipping live test")
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := NewDNSProvider()
|
provider, err := NewDNSProvider()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = provider.Present(duckdnsDomain, "", "123d==")
|
err = provider.Present(duckdnsDomain, "", "123d==")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLiveDuckdnsCleanUp(t *testing.T) {
|
func TestLiveDuckdnsCleanUp(t *testing.T) {
|
||||||
if !duckdnsLiveTest {
|
if !duckdnsLiveTest {
|
||||||
t.Skip("skipping live test")
|
t.Skip("skipping live test")
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Second * 10)
|
time.Sleep(time.Second * 10)
|
||||||
|
|
||||||
provider, err := NewDNSProvider()
|
provider, err := NewDNSProvider()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = provider.CleanUp(duckdnsDomain, "", "123d==")
|
err = provider.CleanUp(duckdnsDomain, "", "123d==")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ func (d *DNSProvider) sendRequest(method, resource string, payload interface{})
|
||||||
req.Header.Set("Auth-Token", d.token)
|
req.Header.Set("Auth-Token", d.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -158,7 +158,7 @@ func (d *DNSProvider) logout() error {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Auth-Token", d.token)
|
req.Header.Set("Auth-Token", d.token)
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -206,12 +206,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.logout()
|
return d.logout()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSProvider) publish(zone, notes string) error {
|
func (d *DNSProvider) publish(zone, notes string) error {
|
||||||
|
@ -222,12 +217,9 @@ func (d *DNSProvider) publish(zone, notes string) error {
|
||||||
|
|
||||||
pub := &publish{Publish: true, Notes: notes}
|
pub := &publish{Publish: true, Notes: notes}
|
||||||
resource := fmt.Sprintf("Zone/%s/", zone)
|
resource := fmt.Sprintf("Zone/%s/", zone)
|
||||||
_, err := d.sendRequest("PUT", resource, pub)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
_, err := d.sendRequest("PUT", resource, pub)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
@ -253,7 +245,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Auth-Token", d.token)
|
req.Header.Set("Auth-Token", d.token)
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -269,10 +261,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.logout()
|
return d.logout()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ type DNSProvider struct {
|
||||||
client *egoscale.Client
|
client *egoscale.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credentials must be passed in the environment variables:
|
// NewDNSProvider Credentials must be passed in the environment variables:
|
||||||
// EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT.
|
// EXOSCALE_API_KEY, EXOSCALE_API_SECRET, EXOSCALE_ENDPOINT.
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
key := os.Getenv("EXOSCALE_API_KEY")
|
key := os.Getenv("EXOSCALE_API_KEY")
|
||||||
|
@ -25,7 +25,7 @@ func NewDNSProvider() (*DNSProvider, error) {
|
||||||
return NewDNSProviderClient(key, secret, endpoint)
|
return NewDNSProviderClient(key, secret, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uses the supplied parameters to return a DNSProvider instance
|
// NewDNSProviderClient Uses the supplied parameters to return a DNSProvider instance
|
||||||
// configured for Exoscale.
|
// configured for Exoscale.
|
||||||
func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) {
|
func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) {
|
||||||
if key == "" || secret == "" {
|
if key == "" || secret == "" {
|
||||||
|
@ -48,7 +48,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
recordID, err := c.FindExistingRecordId(zone, recordName)
|
recordID, err := c.FindExistingRecordID(zone, recordName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
recordID, err := c.FindExistingRecordId(zone, recordName)
|
recordID, err := c.FindExistingRecordID(zone, recordName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -99,9 +99,9 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query Exoscale to find an existing record for this name.
|
// FindExistingRecordID Query Exoscale to find an existing record for this name.
|
||||||
// Returns nil if no record could be found
|
// Returns nil if no record could be found
|
||||||
func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, error) {
|
func (c *DNSProvider) FindExistingRecordID(zone, recordName string) (int64, error) {
|
||||||
records, err := c.client.GetRecords(zone)
|
records, err := c.client.GetRecords(zone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, errors.New("Error while retrievening DNS records: " + err.Error())
|
return -1, errors.New("Error while retrievening DNS records: " + err.Error())
|
||||||
|
@ -114,7 +114,7 @@ func (c *DNSProvider) FindExistingRecordId(zone, recordName string) (int64, erro
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract DNS zone and DNS entry name
|
// FindZoneAndRecordName Extract DNS zone and DNS entry name
|
||||||
func (c *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) {
|
func (c *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) {
|
||||||
zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -74,15 +74,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
if ttl < 300 {
|
if ttl < 300 {
|
||||||
ttl = 300 // 300 is gandi minimum value for ttl
|
ttl = 300 // 300 is gandi minimum value for ttl
|
||||||
}
|
}
|
||||||
|
|
||||||
// find authZone and Gandi zone_id for fqdn
|
// find authZone and Gandi zone_id for fqdn
|
||||||
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
|
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zoneID, err := d.getZoneID(authZone)
|
zoneID, err := d.getZoneID(authZone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine name of TXT record
|
// determine name of TXT record
|
||||||
if !strings.HasSuffix(
|
if !strings.HasSuffix(
|
||||||
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
|
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
|
||||||
|
@ -90,40 +93,49 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
"Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
|
"Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
|
||||||
}
|
}
|
||||||
name := fqdn[:len(fqdn)-len("."+authZone)]
|
name := fqdn[:len(fqdn)-len("."+authZone)]
|
||||||
|
|
||||||
// acquire lock and check there is not a challenge already in
|
// acquire lock and check there is not a challenge already in
|
||||||
// progress for this value of authZone
|
// progress for this value of authZone
|
||||||
d.inProgressMu.Lock()
|
d.inProgressMu.Lock()
|
||||||
defer d.inProgressMu.Unlock()
|
defer d.inProgressMu.Unlock()
|
||||||
|
|
||||||
if _, ok := d.inProgressAuthZones[authZone]; ok {
|
if _, ok := d.inProgressAuthZones[authZone]; ok {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"Gandi DNS: challenge already in progress for authZone %s",
|
"Gandi DNS: challenge already in progress for authZone %s",
|
||||||
authZone)
|
authZone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform API actions to create and activate new gandi zone
|
// perform API actions to create and activate new gandi zone
|
||||||
// containing the required TXT record
|
// containing the required TXT record
|
||||||
newZoneName := fmt.Sprintf(
|
newZoneName := fmt.Sprintf(
|
||||||
"%s [ACME Challenge %s]",
|
"%s [ACME Challenge %s]",
|
||||||
acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
|
acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z))
|
||||||
|
|
||||||
newZoneID, err := d.cloneZone(zoneID, newZoneName)
|
newZoneID, err := d.cloneZone(zoneID, newZoneName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newZoneVersion, err := d.newZoneVersion(newZoneID)
|
newZoneVersion, err := d.newZoneVersion(newZoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, ttl)
|
err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, ttl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.setZoneVersion(newZoneID, newZoneVersion)
|
err = d.setZoneVersion(newZoneID, newZoneVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.setZone(authZone, newZoneID)
|
err = d.setZone(authZone, newZoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// save data necessary for CleanUp
|
// save data necessary for CleanUp
|
||||||
d.inProgressFQDNs[fqdn] = inProgressInfo{
|
d.inProgressFQDNs[fqdn] = inProgressInfo{
|
||||||
zoneID: zoneID,
|
zoneID: zoneID,
|
||||||
|
@ -142,25 +154,25 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
// acquire lock and retrieve zoneID, newZoneID and authZone
|
// acquire lock and retrieve zoneID, newZoneID and authZone
|
||||||
d.inProgressMu.Lock()
|
d.inProgressMu.Lock()
|
||||||
defer d.inProgressMu.Unlock()
|
defer d.inProgressMu.Unlock()
|
||||||
|
|
||||||
if _, ok := d.inProgressFQDNs[fqdn]; !ok {
|
if _, ok := d.inProgressFQDNs[fqdn]; !ok {
|
||||||
// if there is no cleanup information then just return
|
// if there is no cleanup information then just return
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
zoneID := d.inProgressFQDNs[fqdn].zoneID
|
zoneID := d.inProgressFQDNs[fqdn].zoneID
|
||||||
newZoneID := d.inProgressFQDNs[fqdn].newZoneID
|
newZoneID := d.inProgressFQDNs[fqdn].newZoneID
|
||||||
authZone := d.inProgressFQDNs[fqdn].authZone
|
authZone := d.inProgressFQDNs[fqdn].authZone
|
||||||
delete(d.inProgressFQDNs, fqdn)
|
delete(d.inProgressFQDNs, fqdn)
|
||||||
delete(d.inProgressAuthZones, authZone)
|
delete(d.inProgressAuthZones, authZone)
|
||||||
|
|
||||||
// perform API actions to restore old gandi zone for authZone
|
// perform API actions to restore old gandi zone for authZone
|
||||||
err := d.setZone(authZone, zoneID)
|
err := d.setZone(authZone, zoneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = d.deleteZone(newZoneID)
|
|
||||||
if err != nil {
|
return d.deleteZone(newZoneID)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout returns the values (40*time.Minute, 60*time.Second) which
|
// Timeout returns the values (40*time.Minute, 60*time.Second) which
|
||||||
|
@ -259,15 +271,18 @@ func (e rpcError) Error() string {
|
||||||
|
|
||||||
func httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
|
func httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
|
||||||
client := http.Client{Timeout: 60 * time.Second}
|
client := http.Client{Timeout: 60 * time.Second}
|
||||||
|
|
||||||
resp, err := client.Post(url, bodyType, body)
|
resp, err := client.Post(url, bodyType, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
|
return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
|
return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,12 +296,14 @@ func rpcCall(call *methodCall, resp response) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Gandi DNS: Marshal Error: %v", err)
|
return fmt.Errorf("Gandi DNS: Marshal Error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// post
|
// post
|
||||||
b = append([]byte(`<?xml version="1.0"?>`+"\n"), b...)
|
b = append([]byte(`<?xml version="1.0"?>`+"\n"), b...)
|
||||||
respBody, err := httpPost(endpoint, "text/xml", bytes.NewReader(b))
|
respBody, err := httpPost(endpoint, "text/xml", bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal
|
// unmarshal
|
||||||
err = xml.Unmarshal(respBody, resp)
|
err = xml.Unmarshal(respBody, resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -313,12 +330,14 @@ func (d *DNSProvider) getZoneID(domain string) (int, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var zoneID int
|
var zoneID int
|
||||||
for _, member := range resp.StructMembers {
|
for _, member := range resp.StructMembers {
|
||||||
if member.Name == "zone_id" {
|
if member.Name == "zone_id" {
|
||||||
zoneID = member.ValueInt
|
zoneID = member.ValueInt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if zoneID == 0 {
|
if zoneID == 0 {
|
||||||
return 0, fmt.Errorf(
|
return 0, fmt.Errorf(
|
||||||
"Gandi DNS: Could not determine zone_id for %s", domain)
|
"Gandi DNS: Could not determine zone_id for %s", domain)
|
||||||
|
@ -346,12 +365,14 @@ func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var newZoneID int
|
var newZoneID int
|
||||||
for _, member := range resp.StructMembers {
|
for _, member := range resp.StructMembers {
|
||||||
if member.Name == "id" {
|
if member.Name == "id" {
|
||||||
newZoneID = member.ValueInt
|
newZoneID = member.ValueInt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newZoneID == 0 {
|
if newZoneID == 0 {
|
||||||
return 0, fmt.Errorf("Gandi DNS: Could not determine cloned zone_id")
|
return 0, fmt.Errorf("Gandi DNS: Could not determine cloned zone_id")
|
||||||
}
|
}
|
||||||
|
@ -370,6 +391,7 @@ func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Value == 0 {
|
if resp.Value == 0 {
|
||||||
return 0, fmt.Errorf("Gandi DNS: Could not create new zone version")
|
return 0, fmt.Errorf("Gandi DNS: Could not create new zone version")
|
||||||
}
|
}
|
||||||
|
@ -402,10 +424,7 @@ func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value s
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, resp)
|
}, resp)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
|
func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
|
||||||
|
@ -421,6 +440,7 @@ func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !resp.Value {
|
if !resp.Value {
|
||||||
return fmt.Errorf("Gandi DNS: could not set zone version")
|
return fmt.Errorf("Gandi DNS: could not set zone version")
|
||||||
}
|
}
|
||||||
|
@ -440,12 +460,14 @@ func (d *DNSProvider) setZone(domain string, zoneID int) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var respZoneID int
|
var respZoneID int
|
||||||
for _, member := range resp.StructMembers {
|
for _, member := range resp.StructMembers {
|
||||||
if member.Name == "zone_id" {
|
if member.Name == "zone_id" {
|
||||||
respZoneID = member.ValueInt
|
respZoneID = member.ValueInt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if respZoneID != zoneID {
|
if respZoneID != zoneID {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"Gandi DNS: Could not set new zone_id for %s", domain)
|
"Gandi DNS: Could not set new zone_id for %s", domain)
|
||||||
|
@ -465,6 +487,7 @@ func (d *DNSProvider) deleteZone(zoneID int) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !resp.Value {
|
if !resp.Value {
|
||||||
return fmt.Errorf("Gandi DNS: could not delete zone_id")
|
return fmt.Errorf("Gandi DNS: could not delete zone_id")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,15 @@
|
||||||
package gandi
|
package gandi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// stagingServer is the Let's Encrypt staging server used by the live test
|
|
||||||
const stagingServer = "https://acme-staging.api.letsencrypt.org/directory"
|
|
||||||
|
|
||||||
// user implements acme.User and is used by the live test
|
|
||||||
type user struct {
|
|
||||||
Email string
|
|
||||||
Registration *acme.RegistrationResource
|
|
||||||
key crypto.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *user) GetEmail() string {
|
|
||||||
return u.Email
|
|
||||||
}
|
|
||||||
func (u *user) GetRegistration() *acme.RegistrationResource {
|
|
||||||
return u.Registration
|
|
||||||
}
|
|
||||||
func (u *user) GetPrivateKey() crypto.PrivateKey {
|
|
||||||
return u.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
|
// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
|
||||||
// Server, whose responses are predetermined for particular requests.
|
// Server, whose responses are predetermined for particular requests.
|
||||||
func TestDNSProvider(t *testing.T) {
|
func TestDNSProvider(t *testing.T) {
|
||||||
|
@ -92,61 +66,6 @@ func TestDNSProvider(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDNSProviderLive performs a live test to obtain a certificate
|
|
||||||
// using the Let's Encrypt staging server. It runs provided that both
|
|
||||||
// the environment variables GANDI_API_KEY and GANDI_TEST_DOMAIN are
|
|
||||||
// set. Otherwise the test is skipped.
|
|
||||||
//
|
|
||||||
// To complete this test, go test must be run with the -timeout=40m
|
|
||||||
// flag, since the default timeout of 10m is insufficient.
|
|
||||||
func TestDNSProviderLive(t *testing.T) {
|
|
||||||
apiKey := os.Getenv("GANDI_API_KEY")
|
|
||||||
domain := os.Getenv("GANDI_TEST_DOMAIN")
|
|
||||||
if apiKey == "" || domain == "" {
|
|
||||||
t.Skip("skipping live test")
|
|
||||||
}
|
|
||||||
// create a user.
|
|
||||||
const rsaKeySize = 2048
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser := user{
|
|
||||||
Email: "test@example.com",
|
|
||||||
key: privateKey,
|
|
||||||
}
|
|
||||||
// create a client using staging server
|
|
||||||
client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
provider, err := NewDNSProviderCredentials(apiKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = client.SetChallengeProvider(acme.DNS01, provider)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
|
||||||
// register and agree tos
|
|
||||||
reg, err := client.Register()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser.Registration = reg
|
|
||||||
err = client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// complete the challenge
|
|
||||||
bundle := false
|
|
||||||
_, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false)
|
|
||||||
if len(failures) > 0 {
|
|
||||||
t.Fatal(failures)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverResponses is the XML-RPC Request->Response map used by the
|
// serverResponses is the XML-RPC Request->Response map used by the
|
||||||
// fake RPC server. It was generated by recording a real RPC session
|
// fake RPC server. It was generated by recording a real RPC session
|
||||||
// which resulted in the successful issue of a cert, and then
|
// which resulted in the successful issue of a cert, and then
|
||||||
|
|
|
@ -21,6 +21,7 @@ var (
|
||||||
// endpoint is the Gandi API endpoint used by Present and
|
// endpoint is the Gandi API endpoint used by Present and
|
||||||
// CleanUp. It is overridden during tests.
|
// CleanUp. It is overridden during tests.
|
||||||
endpoint = "https://dns.api.gandi.net/api/v5"
|
endpoint = "https://dns.api.gandi.net/api/v5"
|
||||||
|
|
||||||
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
|
// findZoneByFqdn determines the DNS zone of an fqdn. It is overridden
|
||||||
// during tests.
|
// during tests.
|
||||||
findZoneByFqdn = acme.FindZoneByFqdn
|
findZoneByFqdn = acme.FindZoneByFqdn
|
||||||
|
@ -66,11 +67,13 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
if ttl < 300 {
|
if ttl < 300 {
|
||||||
ttl = 300 // 300 is gandi minimum value for ttl
|
ttl = 300 // 300 is gandi minimum value for ttl
|
||||||
}
|
}
|
||||||
|
|
||||||
// find authZone
|
// find authZone
|
||||||
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
|
return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine name of TXT record
|
// determine name of TXT record
|
||||||
if !strings.HasSuffix(
|
if !strings.HasSuffix(
|
||||||
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
|
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
|
||||||
|
@ -78,15 +81,18 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
"Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
|
"Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
|
||||||
}
|
}
|
||||||
name := fqdn[:len(fqdn)-len("."+authZone)]
|
name := fqdn[:len(fqdn)-len("."+authZone)]
|
||||||
|
|
||||||
// acquire lock and check there is not a challenge already in
|
// acquire lock and check there is not a challenge already in
|
||||||
// progress for this value of authZone
|
// progress for this value of authZone
|
||||||
d.inProgressMu.Lock()
|
d.inProgressMu.Lock()
|
||||||
defer d.inProgressMu.Unlock()
|
defer d.inProgressMu.Unlock()
|
||||||
|
|
||||||
// add TXT record into authZone
|
// add TXT record into authZone
|
||||||
err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, ttl)
|
err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, ttl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// save data necessary for CleanUp
|
// save data necessary for CleanUp
|
||||||
d.inProgressFQDNs[fqdn] = inProgressInfo{
|
d.inProgressFQDNs[fqdn] = inProgressInfo{
|
||||||
authZone: authZone,
|
authZone: authZone,
|
||||||
|
@ -98,6 +104,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
// CleanUp removes the TXT record matching the specified parameters.
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
// acquire lock and retrieve authZone
|
// acquire lock and retrieve authZone
|
||||||
d.inProgressMu.Lock()
|
d.inProgressMu.Lock()
|
||||||
defer d.inProgressMu.Unlock()
|
defer d.inProgressMu.Unlock()
|
||||||
|
@ -105,15 +112,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
// if there is no cleanup information then just return
|
// if there is no cleanup information then just return
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldName := d.inProgressFQDNs[fqdn].fieldName
|
fieldName := d.inProgressFQDNs[fqdn].fieldName
|
||||||
authZone := d.inProgressFQDNs[fqdn].authZone
|
authZone := d.inProgressFQDNs[fqdn].authZone
|
||||||
delete(d.inProgressFQDNs, fqdn)
|
delete(d.inProgressFQDNs, fqdn)
|
||||||
|
|
||||||
// delete TXT record from authZone
|
// delete TXT record from authZone
|
||||||
err := d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName)
|
return d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout returns the values (20*time.Minute, 20*time.Second) which
|
// Timeout returns the values (20*time.Minute, 20*time.Second) which
|
||||||
|
@ -149,16 +154,18 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
if len(d.apiKey) > 0 {
|
if len(d.apiKey) > 0 {
|
||||||
req.Header.Set("X-Api-Key", d.apiKey)
|
req.Header.Set("X-Api-Key", d.apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1,41 +1,15 @@
|
||||||
package gandiv5
|
package gandiv5
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// stagingServer is the Let's Encrypt staging server used by the live test
|
|
||||||
const stagingServer = "https://acme-staging.api.letsencrypt.org/directory"
|
|
||||||
|
|
||||||
// user implements acme.User and is used by the live test
|
|
||||||
type user struct {
|
|
||||||
Email string
|
|
||||||
Registration *acme.RegistrationResource
|
|
||||||
key crypto.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *user) GetEmail() string {
|
|
||||||
return u.Email
|
|
||||||
}
|
|
||||||
func (u *user) GetRegistration() *acme.RegistrationResource {
|
|
||||||
return u.Registration
|
|
||||||
}
|
|
||||||
func (u *user) GetPrivateKey() crypto.PrivateKey {
|
|
||||||
return u.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
|
// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
|
||||||
// Server, whose responses are predetermined for particular requests.
|
// Server, whose responses are predetermined for particular requests.
|
||||||
func TestDNSProvider(t *testing.T) {
|
func TestDNSProvider(t *testing.T) {
|
||||||
|
@ -92,61 +66,6 @@ func TestDNSProvider(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDNSProviderLive performs a live test to obtain a certificate
|
|
||||||
// using the Let's Encrypt staging server. It runs provided that both
|
|
||||||
// the environment variables GANDIV5_API_KEY and GANDI_TEST_DOMAIN are
|
|
||||||
// set. Otherwise the test is skipped.
|
|
||||||
//
|
|
||||||
// To complete this test, go test must be run with the -timeout=40m
|
|
||||||
// flag, since the default timeout of 10m is insufficient.
|
|
||||||
func TestDNSProviderLive(t *testing.T) {
|
|
||||||
apiKey := os.Getenv("GANDIV5_API_KEY")
|
|
||||||
domain := os.Getenv("GANDI_TEST_DOMAIN")
|
|
||||||
if apiKey == "" || domain == "" {
|
|
||||||
t.Skip("skipping live test")
|
|
||||||
}
|
|
||||||
// create a user.
|
|
||||||
const rsaKeySize = 2048
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser := user{
|
|
||||||
Email: "test@example.com",
|
|
||||||
key: privateKey,
|
|
||||||
}
|
|
||||||
// create a client using staging server
|
|
||||||
client, err := acme.NewClient(stagingServer, &myUser, acme.RSA2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
provider, err := NewDNSProviderCredentials(apiKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = client.SetChallengeProvider(acme.DNS01, provider)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
|
||||||
// register and agree tos
|
|
||||||
reg, err := client.Register()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
myUser.Registration = reg
|
|
||||||
err = client.AgreeToTOS()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// complete the challenge
|
|
||||||
bundle := false
|
|
||||||
_, failures := client.ObtainCertificate([]string{domain}, bundle, nil, false)
|
|
||||||
if len(failures) > 0 {
|
|
||||||
t.Fatal(failures)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverResponses is the JSON Request->Response map used by the
|
// serverResponses is the JSON Request->Response map used by the
|
||||||
// fake JSON server.
|
// fake JSON server.
|
||||||
var serverResponses = map[string]string{
|
var serverResponses = map[string]string{
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -14,29 +13,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
"github.com/xenolf/lego/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GleSYS API reference: https://github.com/GleSYS/API/wiki/API-Documentation
|
// GleSYS API reference: https://github.com/GleSYS/API/wiki/API-Documentation
|
||||||
|
|
||||||
// domainAPI is the GleSYS API endpoint used by Present and CleanUp.
|
// domainAPI is the GleSYS API endpoint used by Present and CleanUp.
|
||||||
const domainAPI = "https://api.glesys.com/domain"
|
const domainAPI = "https://api.glesys.com/domain"
|
||||||
|
|
||||||
var (
|
|
||||||
// Logger is used to log API communication results;
|
|
||||||
// if nil, the default log.Logger is used.
|
|
||||||
Logger *log.Logger
|
|
||||||
)
|
|
||||||
|
|
||||||
// logf writes a log entry. It uses Logger if not
|
|
||||||
// nil, otherwise it uses the default log.Logger.
|
|
||||||
func logf(format string, args ...interface{}) {
|
|
||||||
if Logger != nil {
|
|
||||||
Logger.Printf(format, args...)
|
|
||||||
} else {
|
|
||||||
log.Printf(format, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNSProvider is an implementation of the
|
// DNSProvider is an implementation of the
|
||||||
// acme.ChallengeProviderTimeout interface that uses GleSYS
|
// acme.ChallengeProviderTimeout interface that uses GleSYS
|
||||||
// API to manage TXT records for a domain.
|
// API to manage TXT records for a domain.
|
||||||
|
@ -80,6 +64,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GleSYS DNS: findZoneByFqdn failure: %v", err)
|
return fmt.Errorf("GleSYS DNS: findZoneByFqdn failure: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine name of TXT record
|
// determine name of TXT record
|
||||||
if !strings.HasSuffix(
|
if !strings.HasSuffix(
|
||||||
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
|
strings.ToLower(fqdn), strings.ToLower("."+authZone)) {
|
||||||
|
@ -87,23 +72,27 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
"GleSYS DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
|
"GleSYS DNS: unexpected authZone %s for fqdn %s", authZone, fqdn)
|
||||||
}
|
}
|
||||||
name := fqdn[:len(fqdn)-len("."+authZone)]
|
name := fqdn[:len(fqdn)-len("."+authZone)]
|
||||||
|
|
||||||
// acquire lock and check there is not a challenge already in
|
// acquire lock and check there is not a challenge already in
|
||||||
// progress for this value of authZone
|
// progress for this value of authZone
|
||||||
d.inProgressMu.Lock()
|
d.inProgressMu.Lock()
|
||||||
defer d.inProgressMu.Unlock()
|
defer d.inProgressMu.Unlock()
|
||||||
|
|
||||||
// add TXT record into authZone
|
// add TXT record into authZone
|
||||||
recordId, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, ttl)
|
recordID, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, ttl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// save data necessary for CleanUp
|
// save data necessary for CleanUp
|
||||||
d.activeRecords[fqdn] = recordId
|
d.activeRecords[fqdn] = recordID
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters.
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||||
|
|
||||||
// acquire lock and retrieve authZone
|
// acquire lock and retrieve authZone
|
||||||
d.inProgressMu.Lock()
|
d.inProgressMu.Lock()
|
||||||
defer d.inProgressMu.Unlock()
|
defer d.inProgressMu.Unlock()
|
||||||
|
@ -111,14 +100,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
// if there is no cleanup information then just return
|
// if there is no cleanup information then just return
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
recordId := d.activeRecords[fqdn]
|
|
||||||
|
recordID := d.activeRecords[fqdn]
|
||||||
delete(d.activeRecords, fqdn)
|
delete(d.activeRecords, fqdn)
|
||||||
|
|
||||||
// delete TXT record from authZone
|
// delete TXT record from authZone
|
||||||
err := d.deleteTXTRecord(domain, recordId)
|
return d.deleteTXTRecord(domain, recordID)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout returns the values (20*time.Minute, 20*time.Second) which
|
// Timeout returns the values (20*time.Minute, 20*time.Second) which
|
||||||
|
@ -135,7 +122,7 @@ type addRecordRequest struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
Ttl int `json:"ttl,omitempty"`
|
TTL int `json:"ttl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type deleteRecordRequest struct {
|
type deleteRecordRequest struct {
|
||||||
|
@ -160,14 +147,16 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
req, err := http.NewRequest(method, url, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.SetBasicAuth(d.apiUser, d.apiKey)
|
req.SetBasicAuth(d.apiUser, d.apiKey)
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -177,6 +166,7 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
return nil, fmt.Errorf("GleSYS DNS: request failed with HTTP status code %d", resp.StatusCode)
|
return nil, fmt.Errorf("GleSYS DNS: request failed with HTTP status code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var response responseStruct
|
var response responseStruct
|
||||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
|
||||||
|
@ -187,14 +177,14 @@ func (d *DNSProvider) sendRequest(method string, resource string, payload interf
|
||||||
|
|
||||||
func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) {
|
func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) {
|
||||||
response, err := d.sendRequest("POST", "addrecord", addRecordRequest{
|
response, err := d.sendRequest("POST", "addrecord", addRecordRequest{
|
||||||
Domainname: domain,
|
Domainname: domain,
|
||||||
Host: name,
|
Host: name,
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
Data: value,
|
Data: value,
|
||||||
Ttl: ttl,
|
TTL: ttl,
|
||||||
})
|
})
|
||||||
if response != nil && response.Response.Status.Code == 200 {
|
if response != nil && response.Response.Status.Code == 200 {
|
||||||
logf("[INFO][%s] GleSYS DNS: Successfully created recordid %d", fqdn, response.Response.Record.Recordid)
|
log.Printf("[INFO][%s] GleSYS DNS: Successfully created recordid %d", fqdn, response.Response.Record.Recordid)
|
||||||
return response.Response.Record.Recordid, nil
|
return response.Response.Record.Recordid, nil
|
||||||
}
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
|
@ -205,7 +195,7 @@ func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error {
|
||||||
Recordid: recordid,
|
Recordid: recordid,
|
||||||
})
|
})
|
||||||
if response != nil && response.Response.Status.Code == 200 {
|
if response != nil && response.Response.Status.Code == 200 {
|
||||||
logf("[INFO][%s] GleSYS DNS: Successfully deleted recordid %d", fqdn, recordid)
|
log.Printf("[INFO][%s] GleSYS DNS: Successfully deleted recordid %d", fqdn, recordid)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,10 @@ import (
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GoDaddyAPIURL represents the API endpoint to call.
|
// GoDaddyAPIURL represents the API endpoint to call.
|
||||||
|
@ -75,7 +76,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
Name: recordName,
|
Name: recordName,
|
||||||
Data: value,
|
Data: value,
|
||||||
Ttl: ttl,
|
TTL: ttl,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +99,7 @@ func (c *DNSProvider) updateRecords(records []DNSRecord, domainZone string, reco
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
bodyBytes, _ := ioutil.ReadAll(resp.Body)
|
bodyBytes, _ := ioutil.ReadAll(resp.Body)
|
||||||
return fmt.Errorf("Could not create record %v; Status: %v; Body: %s\n", string(body), resp.StatusCode, string(bodyBytes))
|
return fmt.Errorf("could not create record %v; Status: %v; Body: %s", string(body), resp.StatusCode, string(bodyBytes))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -146,10 +147,11 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Res
|
||||||
return client.Do(req)
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DNSRecord a DNS record
|
||||||
type DNSRecord struct {
|
type DNSRecord struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
Priority int `json:"priority,omitempty"`
|
Priority int `json:"priority,omitempty"`
|
||||||
Ttl int `json:"ttl,omitempty"`
|
TTL int `json:"ttl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
|
|
||||||
"google.golang.org/api/dns/v1"
|
"google.golang.org/api/dns/v1"
|
||||||
|
@ -74,7 +73,7 @@ func NewDNSProviderServiceAccount(project string, saFile string) (*DNSProvider,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to acquire config: %v", err)
|
return nil, fmt.Errorf("Unable to acquire config: %v", err)
|
||||||
}
|
}
|
||||||
client := conf.Client(oauth2.NoContext)
|
client := conf.Client(context.Background())
|
||||||
|
|
||||||
svc, err := dns.New(client)
|
svc, err := dns.New(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -80,6 +80,7 @@ func TestLiveGoogleCloudPresentMultiple(t *testing.T) {
|
||||||
|
|
||||||
// Check that we're able to create multiple entries
|
// Check that we're able to create multiple entries
|
||||||
err = provider.Present(gcloudDomain, "1", "123d==")
|
err = provider.Present(gcloudDomain, "1", "123d==")
|
||||||
|
assert.NoError(t, err)
|
||||||
err = provider.Present(gcloudDomain, "2", "123d==")
|
err = provider.Present(gcloudDomain, "2", "123d==")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type hostedZoneInfo struct {
|
type hostedZoneInfo struct {
|
||||||
domainId int
|
domainID int
|
||||||
resourceName string
|
resourceName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ func (p *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = p.linode.CreateDomainResourceTXT(zone.domainId, acme.UnFqdn(fqdn), value, 60); err != nil {
|
if _, err = p.linode.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, 60); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all TXT records for the specified domain.
|
// Get all TXT records for the specified domain.
|
||||||
resources, err := p.linode.GetResourcesByType(zone.domainId, "TXT")
|
resources, err := p.linode.GetResourcesByType(zone.domainID, "TXT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ func (p *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if resp.ResourceID != resource.ResourceID {
|
if resp.ResourceID != resource.ResourceID {
|
||||||
return errors.New("Error deleting resource: resource IDs do not match!")
|
return errors.New("error deleting resource: resource IDs do not match")
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ func (p *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &hostedZoneInfo{
|
return &hostedZoneInfo{
|
||||||
domainId: domain.DomainID,
|
domainID: domain.DomainID,
|
||||||
resourceName: resourceName,
|
resourceName: resourceName,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if longest < 1 {
|
if longest < 1 {
|
||||||
return nil, fmt.Errorf("Invalid domain name '%s'", domain)
|
return nil, fmt.Errorf("invalid domain name %q", domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
tld := strings.Join(parts[longest:], ".")
|
tld := strings.Join(parts[longest:], ".")
|
||||||
|
@ -318,7 +318,7 @@ func (d *DNSProvider) setHosts(ch *challenge, hosts []host) error {
|
||||||
shr.Errors[0].Description, shr.Errors[0].Number)
|
shr.Errors[0].Description, shr.Errors[0].Number)
|
||||||
}
|
}
|
||||||
if shr.Result.IsSuccess != "true" {
|
if shr.Result.IsSuccess != "true" {
|
||||||
return fmt.Errorf("Namecheap setHosts failed.")
|
return fmt.Errorf("Namecheap setHosts failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -2,11 +2,12 @@ package otc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fakeOTCUserName = "test"
|
var fakeOTCUserName = "test"
|
||||||
|
@ -15,12 +16,14 @@ var fakeOTCDomainName = "test"
|
||||||
var fakeOTCProjectName = "test"
|
var fakeOTCProjectName = "test"
|
||||||
var fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f"
|
var fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f"
|
||||||
|
|
||||||
|
// DNSMock mock
|
||||||
type DNSMock struct {
|
type DNSMock struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
Server *httptest.Server
|
Server *httptest.Server
|
||||||
Mux *http.ServeMux
|
Mux *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDNSMock create a new DNSMock
|
||||||
func NewDNSMock(t *testing.T) *DNSMock {
|
func NewDNSMock(t *testing.T) *DNSMock {
|
||||||
return &DNSMock{
|
return &DNSMock{
|
||||||
t: t,
|
t: t,
|
||||||
|
@ -38,6 +41,7 @@ func (m *DNSMock) ShutdownServer() {
|
||||||
m.Server.Close()
|
m.Server.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleAuthSuccessfully Handle auth successfully
|
||||||
func (m *DNSMock) HandleAuthSuccessfully() {
|
func (m *DNSMock) HandleAuthSuccessfully() {
|
||||||
m.Mux.HandleFunc("/v3/auth/token", func(w http.ResponseWriter, r *http.Request) {
|
m.Mux.HandleFunc("/v3/auth/token", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("X-Subject-Token", fakeOTCToken)
|
w.Header().Set("X-Subject-Token", fakeOTCToken)
|
||||||
|
@ -64,6 +68,7 @@ func (m *DNSMock) HandleAuthSuccessfully() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleListZonesSuccessfully Handle list zones successfully
|
||||||
func (m *DNSMock) HandleListZonesSuccessfully() {
|
func (m *DNSMock) HandleListZonesSuccessfully() {
|
||||||
m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) {
|
m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, `{
|
fmt.Fprintf(w, `{
|
||||||
|
@ -79,6 +84,7 @@ func (m *DNSMock) HandleListZonesSuccessfully() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleListZonesEmpty Handle list zones empty
|
||||||
func (m *DNSMock) HandleListZonesEmpty() {
|
func (m *DNSMock) HandleListZonesEmpty() {
|
||||||
m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) {
|
m.Mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, `{
|
fmt.Fprintf(w, `{
|
||||||
|
@ -93,6 +99,7 @@ func (m *DNSMock) HandleListZonesEmpty() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleDeleteRecordsetsSuccessfully Handle delete recordsets successfully
|
||||||
func (m *DNSMock) HandleDeleteRecordsetsSuccessfully() {
|
func (m *DNSMock) HandleDeleteRecordsetsSuccessfully() {
|
||||||
m.Mux.HandleFunc("/v2/zones/123123/recordsets/321321", func(w http.ResponseWriter, r *http.Request) {
|
m.Mux.HandleFunc("/v2/zones/123123/recordsets/321321", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, `{
|
fmt.Fprintf(w, `{
|
||||||
|
@ -107,6 +114,7 @@ func (m *DNSMock) HandleDeleteRecordsetsSuccessfully() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleListRecordsetsEmpty Handle list recordsets empty
|
||||||
func (m *DNSMock) HandleListRecordsetsEmpty() {
|
func (m *DNSMock) HandleListRecordsetsEmpty() {
|
||||||
m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) {
|
m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, `{
|
fmt.Fprintf(w, `{
|
||||||
|
@ -118,6 +126,8 @@ func (m *DNSMock) HandleListRecordsetsEmpty() {
|
||||||
assert.Equal(m.t, r.URL.RawQuery, "type=TXT&name=_acme-challenge.example.com.")
|
assert.Equal(m.t, r.URL.RawQuery, "type=TXT&name=_acme-challenge.example.com.")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleListRecordsetsSuccessfully Handle list recordsets successfully
|
||||||
func (m *DNSMock) HandleListRecordsetsSuccessfully() {
|
func (m *DNSMock) HandleListRecordsetsSuccessfully() {
|
||||||
m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) {
|
m.Mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
|
|
|
@ -59,6 +59,7 @@ func NewDNSProviderCredentials(domainName, userName, password, projectName, iden
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendRequest send request
|
||||||
func (d *DNSProvider) SendRequest(method, resource string, payload interface{}) (io.Reader, error) {
|
func (d *DNSProvider) SendRequest(method, resource string, payload interface{}) (io.Reader, error) {
|
||||||
url := fmt.Sprintf("%s/%s", d.otcBaseURL, resource)
|
url := fmt.Sprintf("%s/%s", d.otcBaseURL, resource)
|
||||||
|
|
||||||
|
@ -81,7 +82,7 @@ func (d *DNSProvider) SendRequest(method, resource string, payload interface{})
|
||||||
tr.DisableKeepAlives = true
|
tr.DisableKeepAlives = true
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: time.Duration(10 * time.Second),
|
Timeout: 10 * time.Second,
|
||||||
Transport: tr,
|
Transport: tr,
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
@ -168,7 +169,7 @@ func (d *DNSProvider) loginRequest() error {
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Duration(10 * time.Second)}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -221,12 +222,7 @@ func (d *DNSProvider) loginRequest() error {
|
||||||
// Starts a new OTC API Session. Authenticates using userName, password
|
// Starts a new OTC API Session. Authenticates using userName, password
|
||||||
// and receives a token to be used in for subsequent requests.
|
// and receives a token to be used in for subsequent requests.
|
||||||
func (d *DNSProvider) login() error {
|
func (d *DNSProvider) login() error {
|
||||||
err := d.loginRequest()
|
return d.loginRequest()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSProvider) getZoneID(zone string) (string, error) {
|
func (d *DNSProvider) getZoneID(zone string) (string, error) {
|
||||||
|
@ -305,10 +301,7 @@ func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error {
|
||||||
resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID)
|
resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID)
|
||||||
|
|
||||||
_, err := d.SendRequest("DELETE", resource, nil)
|
_, err := d.SendRequest("DELETE", resource, nil)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present creates a TXT record using the specified parameters
|
// Present creates a TXT record using the specified parameters
|
||||||
|
@ -340,7 +333,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Ttl int `json:"ttl"`
|
TTL int `json:"ttl"`
|
||||||
Records []string `json:"records"`
|
Records []string `json:"records"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,16 +341,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
Name: fqdn,
|
Name: fqdn,
|
||||||
Description: "Added TXT record for ACME dns-01 challenge using lego client",
|
Description: "Added TXT record for ACME dns-01 challenge using lego client",
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
Ttl: 300,
|
TTL: ttl,
|
||||||
Records: []string{fmt.Sprintf("\"%s\"", value)},
|
Records: []string{fmt.Sprintf("\"%s\"", value)},
|
||||||
}
|
}
|
||||||
_, err = d.SendRequest("POST", resource, r1)
|
_, err = d.SendRequest("POST", resource, r1)
|
||||||
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
@ -375,7 +363,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
zoneID, err := d.getZoneID(authZone)
|
zoneID, err := d.getZoneID(authZone)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -384,5 +371,6 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable go get record %s for zone %s: %s", fqdn, domain, err)
|
return fmt.Errorf("unable go get record %s for zone %s: %s", fqdn, domain, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.deleteRecordSet(zoneID, recordID)
|
return d.deleteRecordSet(zoneID, recordID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Package OVH implements a DNS provider for solving the DNS-01
|
// Package ovh implements a DNS provider for solving the DNS-01
|
||||||
// challenge using OVH DNS.
|
// challenge using OVH DNS.
|
||||||
package ovh
|
package ovh
|
||||||
|
|
||||||
|
|
|
@ -29,12 +29,12 @@ type DNSProvider struct {
|
||||||
// PDNS_API_URL and PDNS_API_KEY.
|
// PDNS_API_URL and PDNS_API_KEY.
|
||||||
func NewDNSProvider() (*DNSProvider, error) {
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
key := os.Getenv("PDNS_API_KEY")
|
key := os.Getenv("PDNS_API_KEY")
|
||||||
hostUrl, err := url.Parse(os.Getenv("PDNS_API_URL"))
|
hostURL, err := url.Parse(os.Getenv("PDNS_API_URL"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewDNSProviderCredentials(hostUrl, key)
|
return NewDNSProviderCredentials(hostURL, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDNSProviderCredentials uses the supplied credentials to return a
|
// NewDNSProviderCredentials uses the supplied credentials to return a
|
||||||
|
@ -107,12 +107,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body))
|
_, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body))
|
||||||
if err != nil {
|
return err
|
||||||
fmt.Println("here")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
@ -131,7 +126,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
|
||||||
rrsets := rrSets{
|
rrsets := rrSets{
|
||||||
RRSets: []rrSet{
|
RRSets: []rrSet{
|
||||||
rrSet{
|
{
|
||||||
Name: set.Name,
|
Name: set.Name,
|
||||||
Type: set.Type,
|
Type: set.Type,
|
||||||
ChangeType: "DELETE",
|
ChangeType: "DELETE",
|
||||||
|
@ -220,7 +215,7 @@ func (c *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("No existing record found for %s", fqdn)
|
return nil, fmt.Errorf("no existing record found for %s", fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DNSProvider) getAPIVersion() {
|
func (c *DNSProvider) getAPIVersion() {
|
||||||
|
|
|
@ -92,7 +92,7 @@ func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) {
|
||||||
client := http.Client{Timeout: 30 * time.Second}
|
client := http.Client{Timeout: 30 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error querying Rackspace Identity API: %v", err)
|
return nil, fmt.Errorf("error querying Rackspace Identity API: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dnsEndpoint == "" {
|
if dnsEndpoint == "" {
|
||||||
return nil, fmt.Errorf("Failed to populate DNS endpoint, check Rackspace API for changes.")
|
return nil, fmt.Errorf("failed to populate DNS endpoint, check Rackspace API for changes")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DNSProvider{
|
return &DNSProvider{
|
||||||
|
@ -132,8 +132,8 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rec := RackspaceRecords{
|
rec := Records{
|
||||||
RackspaceRecord: []RackspaceRecord{{
|
Record: []Record{{
|
||||||
Name: acme.UnFqdn(fqdn),
|
Name: acme.UnFqdn(fqdn),
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
Data: value,
|
Data: value,
|
||||||
|
@ -147,11 +147,7 @@ func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.makeRequest("POST", fmt.Sprintf("/domains/%d/records", zoneID), bytes.NewReader(body))
|
_, err = c.makeRequest("POST", fmt.Sprintf("/domains/%d/records", zoneID), bytes.NewReader(body))
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
|
@ -168,11 +164,7 @@ func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.makeRequest("DELETE", fmt.Sprintf("/domains/%d/records?id=%s", zoneID, record.ID), nil)
|
_, err = c.makeRequest("DELETE", fmt.Sprintf("/domains/%d/records?id=%s", zoneID, record.ID), nil)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHostedZoneID performs a lookup to get the DNS zone which needs
|
// getHostedZoneID performs a lookup to get the DNS zone which needs
|
||||||
|
@ -205,36 +197,35 @@ func (c *DNSProvider) getHostedZoneID(fqdn string) (int, error) {
|
||||||
|
|
||||||
// If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur)
|
// If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur)
|
||||||
if zoneSearchResponse.TotalEntries != 1 {
|
if zoneSearchResponse.TotalEntries != 1 {
|
||||||
return 0, fmt.Errorf("Found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn)
|
return 0, fmt.Errorf("found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return zoneSearchResponse.HostedZones[0].ID, nil
|
return zoneSearchResponse.HostedZones[0].ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findTxtRecord searches a DNS zone for a TXT record with a specific name
|
// findTxtRecord searches a DNS zone for a TXT record with a specific name
|
||||||
func (c *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*RackspaceRecord, error) {
|
func (c *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*Record, error) {
|
||||||
result, err := c.makeRequest("GET", fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil)
|
result, err := c.makeRequest("GET", fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var records RackspaceRecords
|
var records Records
|
||||||
err = json.Unmarshal(result, &records)
|
err = json.Unmarshal(result, &records)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
recordsLength := len(records.RackspaceRecord)
|
recordsLength := len(records.Record)
|
||||||
switch recordsLength {
|
switch recordsLength {
|
||||||
case 1:
|
case 1:
|
||||||
break
|
|
||||||
case 0:
|
case 0:
|
||||||
return nil, fmt.Errorf("No TXT record found for %s", fqdn)
|
return nil, fmt.Errorf("no TXT record found for %s", fqdn)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("More than 1 TXT record found for %s", fqdn)
|
return nil, fmt.Errorf("more than 1 TXT record found for %s", fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &records.RackspaceRecord[0], nil
|
return &records.Record[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeRequest is a wrapper function used for making DNS API requests
|
// makeRequest is a wrapper function used for making DNS API requests
|
||||||
|
@ -251,13 +242,13 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
|
||||||
client := http.Client{Timeout: 30 * time.Second}
|
client := http.Client{Timeout: 30 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error querying DNS API: %v", err)
|
return nil, fmt.Errorf("error querying DNS API: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||||
return nil, fmt.Errorf("Request failed for %s %s. Response code: %d", method, url, resp.StatusCode)
|
return nil, fmt.Errorf("request failed for %s %s. Response code: %d", method, url, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var r json.RawMessage
|
var r json.RawMessage
|
||||||
|
@ -269,13 +260,13 @@ func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawM
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RackspaceRecords is the list of records sent/received from the DNS API
|
// Records is the list of records sent/received from the DNS API
|
||||||
type RackspaceRecords struct {
|
type Records struct {
|
||||||
RackspaceRecord []RackspaceRecord `json:"records"`
|
Record []Record `json:"records"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RackspaceRecord represents a Rackspace DNS record
|
// Record represents a Rackspace DNS record
|
||||||
type RackspaceRecord struct {
|
type Record struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
|
|
|
@ -42,13 +42,14 @@ func liveRackspaceEnv() {
|
||||||
os.Setenv("RACKSPACE_API_KEY", rackspaceAPIKey)
|
os.Setenv("RACKSPACE_API_KEY", rackspaceAPIKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTestServers() (identityAPI, dnsAPI *httptest.Server) {
|
func startTestServers() (*httptest.Server, *httptest.Server) {
|
||||||
dnsAPI = httptest.NewServer(dnsMux())
|
dnsAPI := httptest.NewServer(dnsMux())
|
||||||
dnsEndpoint := dnsAPI.URL + "/123456"
|
dnsEndpoint := dnsAPI.URL + "/123456"
|
||||||
|
|
||||||
identityAPI = httptest.NewServer(identityHandler(dnsEndpoint))
|
identityAPI := httptest.NewServer(identityHandler(dnsEndpoint))
|
||||||
testAPIURL = identityAPI.URL + "/"
|
testAPIURL = identityAPI.URL + "/"
|
||||||
return
|
|
||||||
|
return identityAPI, dnsAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeTestServers(identityAPI, dnsAPI *httptest.Server) {
|
func closeTestServers(identityAPI, dnsAPI *httptest.Server) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ type DNSProvider struct {
|
||||||
// dynamic update. Configured with environment variables:
|
// dynamic update. Configured with environment variables:
|
||||||
// RFC2136_NAMESERVER: Network address in the form "host" or "host:port".
|
// RFC2136_NAMESERVER: Network address in the form "host" or "host:port".
|
||||||
// RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5).
|
// RFC2136_TSIG_ALGORITHM: Defaults to hmac-md5.sig-alg.reg.int. (HMAC-MD5).
|
||||||
// See https://github.com/miekg/dns/blob/master/tsig.go for supported values.
|
// See https://github.com/miekg/dns/blob/master/tsig.go for supported values.
|
||||||
// RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration.
|
// RFC2136_TSIG_KEY: Name of the secret key as defined in DNS server configuration.
|
||||||
// RFC2136_TSIG_SECRET: Secret key payload.
|
// RFC2136_TSIG_SECRET: Secret key payload.
|
||||||
// RFC2136_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s)
|
// RFC2136_TIMEOUT: DNS propagation timeout in time.ParseDuration format. (60s)
|
||||||
|
@ -77,7 +77,7 @@ func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, t
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if t < 0 {
|
} else if t < 0 {
|
||||||
return nil, fmt.Errorf("Invalid/negative RFC2136_TIMEOUT: %v", timeout)
|
return nil, fmt.Errorf("invalid/negative RFC2136_TIMEOUT: %v", timeout)
|
||||||
} else {
|
} else {
|
||||||
d.timeout = t
|
d.timeout = t
|
||||||
}
|
}
|
||||||
|
@ -86,26 +86,26 @@ func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, t
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the timeout configured with RFC2136_TIMEOUT, or 60s.
|
// Timeout Returns the timeout configured with RFC2136_TIMEOUT, or 60s.
|
||||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
return d.timeout, 2 * time.Second
|
return d.timeout, 2 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present creates a TXT record using the specified parameters
|
// Present creates a TXT record using the specified parameters
|
||||||
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
return r.changeRecord("INSERT", fqdn, value, ttl)
|
return d.changeRecord("INSERT", fqdn, value, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters
|
// CleanUp removes the TXT record matching the specified parameters
|
||||||
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||||
return r.changeRecord("REMOVE", fqdn, value, ttl)
|
return d.changeRecord("REMOVE", fqdn, value, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
||||||
// Find the zone for the given fqdn
|
// Find the zone for the given fqdn
|
||||||
zone, err := acme.FindZoneByFqdn(fqdn, []string{r.nameserver})
|
zone, err := acme.FindZoneByFqdn(fqdn, []string{d.nameserver})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -127,20 +127,20 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
||||||
case "REMOVE":
|
case "REMOVE":
|
||||||
m.Remove(rrs)
|
m.Remove(rrs)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unexpected action: %s", action)
|
return fmt.Errorf("unexpected action: %s", action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup client
|
// Setup client
|
||||||
c := new(dns.Client)
|
c := new(dns.Client)
|
||||||
c.SingleInflight = true
|
c.SingleInflight = true
|
||||||
// TSIG authentication / msg signing
|
// TSIG authentication / msg signing
|
||||||
if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 {
|
if len(d.tsigKey) > 0 && len(d.tsigSecret) > 0 {
|
||||||
m.SetTsig(dns.Fqdn(r.tsigKey), r.tsigAlgorithm, 300, time.Now().Unix())
|
m.SetTsig(dns.Fqdn(d.tsigKey), d.tsigAlgorithm, 300, time.Now().Unix())
|
||||||
c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret}
|
c.TsigSecret = map[string]string{dns.Fqdn(d.tsigKey): d.tsigSecret}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the query
|
// Send the query
|
||||||
reply, _, err := c.Exchange(m, r.nameserver)
|
reply, _, err := c.Exchange(m, d.nameserver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("DNS update failed: %v", err)
|
return fmt.Errorf("DNS update failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@ func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
||||||
hostedZoneID, err := r.getHostedZoneID(fqdn)
|
hostedZoneID, err := r.getHostedZoneID(fqdn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to determine Route 53 hosted zone ID: %v", err)
|
return fmt.Errorf("failed to determine Route 53 hosted zone ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
recordSet := newTXTRecordSet(fqdn, value, ttl)
|
recordSet := newTXTRecordSet(fqdn, value, ttl)
|
||||||
|
@ -118,7 +118,7 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
||||||
|
|
||||||
resp, err := r.client.ChangeResourceRecordSets(reqParams)
|
resp, err := r.client.ChangeResourceRecordSets(reqParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to change Route 53 record set: %v", err)
|
return fmt.Errorf("failed to change Route 53 record set: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusID := resp.ChangeInfo.Id
|
statusID := resp.ChangeInfo.Id
|
||||||
|
@ -129,9 +129,9 @@ func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
||||||
}
|
}
|
||||||
resp, err := r.client.GetChange(reqParams)
|
resp, err := r.client.GetChange(reqParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("Failed to query Route 53 change status: %v", err)
|
return false, fmt.Errorf("failed to query Route 53 change status: %v", err)
|
||||||
}
|
}
|
||||||
if *resp.ChangeInfo.Status == route53.ChangeStatusInsync {
|
if aws.StringValue(resp.ChangeInfo.Status) == route53.ChangeStatusInsync {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -160,14 +160,14 @@ func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
||||||
var hostedZoneID string
|
var hostedZoneID string
|
||||||
for _, hostedZone := range resp.HostedZones {
|
for _, hostedZone := range resp.HostedZones {
|
||||||
// .Name has a trailing dot
|
// .Name has a trailing dot
|
||||||
if !*hostedZone.Config.PrivateZone && *hostedZone.Name == authZone {
|
if !aws.BoolValue(hostedZone.Config.PrivateZone) && aws.StringValue(hostedZone.Name) == authZone {
|
||||||
hostedZoneID = *hostedZone.Id
|
hostedZoneID = aws.StringValue(hostedZone.Id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(hostedZoneID) == 0 {
|
if len(hostedZoneID) == 0 {
|
||||||
return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn)
|
return "", fmt.Errorf("zone %s not found in Route 53 for domain %s", authZone, fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(hostedZoneID, "/hostedzone/") {
|
if strings.HasPrefix(hostedZoneID, "/hostedzone/") {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRoute53TTL(t *testing.T) {
|
func TestRoute53TTL(t *testing.T) {
|
||||||
|
|
||||||
m, err := testGetAndPreCheck()
|
m, err := testGetAndPreCheck()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip(err.Error())
|
t.Skip(err.Error())
|
||||||
|
@ -19,13 +18,14 @@ func TestRoute53TTL(t *testing.T) {
|
||||||
|
|
||||||
provider, err := NewDNSProvider()
|
provider, err := NewDNSProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Fatal: %s", err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.Present(m["route53Domain"], "foo", "bar")
|
err = provider.Present(m["route53Domain"], "foo", "bar")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Fatal: %s", err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need a separate R53 client here as the one in the DNS provider is
|
// we need a separate R53 client here as the one in the DNS provider is
|
||||||
// unexported.
|
// unexported.
|
||||||
fqdn := "_acme-challenge." + m["route53Domain"] + "."
|
fqdn := "_acme-challenge." + m["route53Domain"] + "."
|
||||||
|
@ -33,23 +33,25 @@ func TestRoute53TTL(t *testing.T) {
|
||||||
zoneID, err := provider.getHostedZoneID(fqdn)
|
zoneID, err := provider.getHostedZoneID(fqdn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
||||||
t.Fatalf("Fatal: %s", err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := &route53.ListResourceRecordSetsInput{
|
params := &route53.ListResourceRecordSetsInput{
|
||||||
HostedZoneId: aws.String(zoneID),
|
HostedZoneId: aws.String(zoneID),
|
||||||
}
|
}
|
||||||
resp, err := svc.ListResourceRecordSets(params)
|
resp, err := svc.ListResourceRecordSets(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
||||||
t.Fatalf("Fatal: %s", err.Error())
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range resp.ResourceRecordSets {
|
for _, v := range resp.ResourceRecordSets {
|
||||||
if *v.Name == fqdn && *v.Type == "TXT" && *v.TTL == 10 {
|
if aws.StringValue(v.Name) == fqdn && aws.StringValue(v.Type) == "TXT" && aws.Int64Value(v.TTL) == 10 {
|
||||||
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
provider.CleanUp(m["route53Domain"], "foo", "bar")
|
||||||
t.Fatalf("Could not find a TXT record for _acme-challenge.%s with a TTL of 10", m["route53Domain"])
|
t.Fatalf("Could not find a TXT record for _acme-challenge.%s with a TTL of 10", m["route53Domain"])
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ func TestRegionFromEnv(t *testing.T) {
|
||||||
os.Setenv("AWS_REGION", "us-east-1")
|
os.Setenv("AWS_REGION", "us-east-1")
|
||||||
|
|
||||||
sess := session.New(aws.NewConfig())
|
sess := session.New(aws.NewConfig())
|
||||||
assert.Equal(t, "us-east-1", *sess.Config.Region, "Expected Region to be set from environment")
|
assert.Equal(t, "us-east-1", aws.StringValue(sess.Config.Region), "Expected Region to be set from environment")
|
||||||
|
|
||||||
restoreRoute53Env()
|
restoreRoute53Env()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,18 +10,18 @@ import (
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPProvider implements ChallengeProvider for `http-01` challenge
|
// HTTPProvider implements HTTPProvider for `http-01` challenge
|
||||||
type MemcachedProvider struct {
|
type HTTPProvider struct {
|
||||||
hosts []string
|
hosts []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTPProvider returns a HTTPProvider instance with a configured webroot path
|
// NewMemcachedProvider returns a HTTPProvider instance with a configured webroot path
|
||||||
func NewMemcachedProvider(hosts []string) (*MemcachedProvider, error) {
|
func NewMemcachedProvider(hosts []string) (*HTTPProvider, error) {
|
||||||
if len(hosts) == 0 {
|
if len(hosts) == 0 {
|
||||||
return nil, fmt.Errorf("No memcached hosts provided")
|
return nil, fmt.Errorf("no memcached hosts provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &MemcachedProvider{
|
c := &HTTPProvider{
|
||||||
hosts: hosts,
|
hosts: hosts,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ func NewMemcachedProvider(hosts []string) (*MemcachedProvider, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path
|
// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path
|
||||||
func (w *MemcachedProvider) Present(domain, token, keyAuth string) error {
|
func (w *HTTPProvider) Present(domain, token, keyAuth string) error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
challengePath := path.Join("/", acme.HTTP01ChallengePath(token))
|
challengePath := path.Join("/", acme.HTTP01ChallengePath(token))
|
||||||
|
@ -39,7 +39,7 @@ func (w *MemcachedProvider) Present(domain, token, keyAuth string) error {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mc.Add(&memcache.Item{
|
_ = mc.Add(&memcache.Item{
|
||||||
Key: challengePath,
|
Key: challengePath,
|
||||||
Value: []byte(keyAuth),
|
Value: []byte(keyAuth),
|
||||||
Expiration: 60,
|
Expiration: 60,
|
||||||
|
@ -47,14 +47,14 @@ func (w *MemcachedProvider) Present(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) == len(w.hosts) {
|
if len(errs) == len(w.hosts) {
|
||||||
return fmt.Errorf("Unable to store key in any of the memcache hosts -> %v", errs)
|
return fmt.Errorf("unable to store key in any of the memcache hosts -> %v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the file created for the challenge
|
// CleanUp removes the file created for the challenge
|
||||||
func (w *MemcachedProvider) CleanUp(domain, token, keyAuth string) error {
|
func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
// Memcached will clean up itself, that's what expiration is for.
|
// Memcached will clean up itself, that's what expiration is for.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ func init() {
|
||||||
func TestNewMemcachedProviderEmpty(t *testing.T) {
|
func TestNewMemcachedProviderEmpty(t *testing.T) {
|
||||||
emptyHosts := make([]string, 0)
|
emptyHosts := make([]string, 0)
|
||||||
_, err := NewMemcachedProvider(emptyHosts)
|
_, err := NewMemcachedProvider(emptyHosts)
|
||||||
assert.EqualError(t, err, "No memcached hosts provided")
|
assert.EqualError(t, err, "no memcached hosts provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewMemcachedProviderValid(t *testing.T) {
|
func TestNewMemcachedProviderValid(t *testing.T) {
|
||||||
|
|
|
@ -35,12 +35,12 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error {
|
||||||
challengeFilePath := path.Join(w.path, acme.HTTP01ChallengePath(token))
|
challengeFilePath := path.Join(w.path, acme.HTTP01ChallengePath(token))
|
||||||
err = os.MkdirAll(path.Dir(challengeFilePath), 0755)
|
err = os.MkdirAll(path.Dir(challengeFilePath), 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Could not create required directories in webroot for HTTP challenge -> %v", err)
|
return fmt.Errorf("could not create required directories in webroot for HTTP challenge -> %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(challengeFilePath, []byte(keyAuth), 0644)
|
err = ioutil.WriteFile(challengeFilePath, []byte(keyAuth), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Could not write file in webroot for HTTP challenge -> %v", err)
|
return fmt.Errorf("could not write file in webroot for HTTP challenge -> %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -48,10 +48,9 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
|
||||||
// CleanUp removes the file created for the challenge
|
// CleanUp removes the file created for the challenge
|
||||||
func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
|
func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
var err error
|
err := os.Remove(path.Join(w.path, acme.HTTP01ChallengePath(token)))
|
||||||
err = os.Remove(path.Join(w.path, acme.HTTP01ChallengePath(token)))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Could not remove file in webroot after HTTP challenge -> %v", err)
|
return fmt.Errorf("could not remove file in webroot after HTTP challenge -> %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in a new issue