diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2607a47..434e784 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -164,6 +164,11 @@ "Comment": "v0.3.0-34-gca19a90", "Rev": "ca19a90028e242e878585941c2a27c8f3b3efc25" }, + { + "ImportPath": "github.com/xenolf/lego/providers/dns/dyn", + "Comment": "v0.3.0-34-gca19a90", + "Rev": "ca19a90028e242e878585941c2a27c8f3b3efc25" + }, { "ImportPath": "github.com/xenolf/lego/providers/dns/route53", "Comment": "v0.3.0-34-gca19a90", diff --git a/Godeps/_workspace/src/github.com/xenolf/lego/providers/dns/dyn/dyn.go b/Godeps/_workspace/src/github.com/xenolf/lego/providers/dns/dyn/dyn.go new file mode 100644 index 0000000..86daf96 --- /dev/null +++ b/Godeps/_workspace/src/github.com/xenolf/lego/providers/dns/dyn/dyn.go @@ -0,0 +1,264 @@ +// Package dyn implements a DNS provider for solving the DNS-01 challenge +// using Dyn Managed DNS. +package dyn + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "strconv" + "time" + + "github.com/xenolf/lego/acme" +) + +var dynBaseURL = "https://api.dynect.net/REST" + +type dynResponse struct { + // One of 'success', 'failure', or 'incomplete' + Status string `json:"status"` + + // The structure containing the actual results of the request + Data json.RawMessage `json:"data"` + + // The ID of the job that was created in response to a request. + JobID int `json:"job_id"` + + // A list of zero or more messages + Messages json.RawMessage `json:"msgs"` +} + +// DNSProvider is an implementation of the acme.ChallengeProvider interface that uses +// Dyn's Managed DNS API to manage TXT records for a domain. +type DNSProvider struct { + customerName string + userName string + password string + token string +} + +// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS. +// Credentials must be passed in the environment variables: DYN_CUSTOMER_NAME, +// DYN_USER_NAME and DYN_PASSWORD. +func NewDNSProvider() (*DNSProvider, error) { + customerName := os.Getenv("DYN_CUSTOMER_NAME") + userName := os.Getenv("DYN_USER_NAME") + password := os.Getenv("DYN_PASSWORD") + return NewDNSProviderCredentials(customerName, userName, password) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for Dyn DNS. +func NewDNSProviderCredentials(customerName, userName, password string) (*DNSProvider, error) { + if customerName == "" || userName == "" || password == "" { + return nil, fmt.Errorf("DynDNS credentials missing") + } + + return &DNSProvider{ + customerName: customerName, + userName: userName, + password: password, + }, nil +} + +func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) { + url := fmt.Sprintf("%s/%s", dynBaseURL, resource) + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + if len(d.token) > 0 { + req.Header.Set("Auth-Token", d.token) + } + + client := &http.Client{Timeout: time.Duration(10 * time.Second)} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("Dyn API request failed with HTTP status code %d", resp.StatusCode) + } else if resp.StatusCode == 307 { + // TODO add support for HTTP 307 response and long running jobs + return nil, fmt.Errorf("Dyn API request returned HTTP 307. This is currently unsupported") + } + + var dynRes dynResponse + err = json.NewDecoder(resp.Body).Decode(&dynRes) + if err != nil { + return nil, err + } + + if dynRes.Status == "failure" { + // TODO add better error handling + return nil, fmt.Errorf("Dyn API request failed: %s", dynRes.Messages) + } + + return &dynRes, nil +} + +// Starts a new Dyn API Session. Authenticates using customerName, userName, +// password and receives a token to be used in for subsequent requests. +func (d *DNSProvider) login() error { + type creds struct { + Customer string `json:"customer_name"` + User string `json:"user_name"` + Pass string `json:"password"` + } + + type session struct { + Token string `json:"token"` + Version string `json:"version"` + } + + payload := &creds{Customer: d.customerName, User: d.userName, Pass: d.password} + dynRes, err := d.sendRequest("POST", "Session", payload) + if err != nil { + return err + } + + var s session + err = json.Unmarshal(dynRes.Data, &s) + if err != nil { + return err + } + + d.token = s.Token + + return nil +} + +// Destroys Dyn Session +func (d *DNSProvider) logout() error { + if len(d.token) == 0 { + // nothing to do + return nil + } + + url := fmt.Sprintf("%s/Session", dynBaseURL) + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Auth-Token", d.token) + + client := &http.Client{Timeout: time.Duration(10 * time.Second)} + resp, err := client.Do(req) + if err != nil { + return err + } + resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("Dyn API request failed to delete session with HTTP status code %d", resp.StatusCode) + } + + d.token = "" + + return nil +} + +// Present creates a TXT record using the specified parameters +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + err := d.login() + if err != nil { + return err + } + + fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) + + data := map[string]interface{}{ + "rdata": map[string]string{ + "txtdata": value, + }, + "ttl": strconv.Itoa(ttl), + } + + resource := fmt.Sprintf("TXTRecord/%s/%s/", domain, fqdn) + _, err = d.sendRequest("POST", resource, data) + if err != nil { + return err + } + + err = d.publish(domain, "Added TXT record for ACME dns-01 challenge using lego client") + if err != nil { + return err + } + + err = d.logout() + if err != nil { + return err + } + + return nil +} + +func (d *DNSProvider) publish(domain, notes string) error { + type publish struct { + Publish bool `json:"publish"` + Notes string `json:"notes"` + } + + pub := &publish{Publish: true, Notes: notes} + resource := fmt.Sprintf("Zone/%s/", domain) + _, err := d.sendRequest("PUT", resource, pub) + if err != nil { + return err + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + err := d.login() + if err != nil { + return err + } + + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + resource := fmt.Sprintf("TXTRecord/%s/%s/", domain, fqdn) + url := fmt.Sprintf("%s/%s", dynBaseURL, resource) + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Auth-Token", d.token) + + client := &http.Client{Timeout: time.Duration(10 * time.Second)} + resp, err := client.Do(req) + if err != nil { + return err + } + resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("Dyn API request failed to delete TXT record HTTP status code %d", resp.StatusCode) + } + + err = d.publish(domain, "Removed TXT record for ACME dns-01 challenge using lego client") + if err != nil { + return err + } + + err = d.logout() + if err != nil { + return err + } + + return nil +} diff --git a/context.go b/context.go index e593004..0276280 100644 --- a/context.go +++ b/context.go @@ -89,6 +89,9 @@ func (c *Context) InitContext() { AwsSecretKey: os.Getenv("AWS_SECRET_KEY"), DNSimpleEmail: os.Getenv("DNSIMPLE_EMAIL"), DNSimpleKey: os.Getenv("DNSIMPLE_KEY"), + DynCustomerName: os.Getenv("DYN_CUSTOMER_NAME"), + DynUserName: os.Getenv("DYN_USER_NAME"), + DynPassword: os.Getenv("DYN_PASSWORD"), AwsRegionName: "us-east-1", } diff --git a/letsencrypt/providers.go b/letsencrypt/providers.go index 3346f1f..b54a8c7 100644 --- a/letsencrypt/providers.go +++ b/letsencrypt/providers.go @@ -8,6 +8,7 @@ import ( "github.com/xenolf/lego/providers/dns/cloudflare" "github.com/xenolf/lego/providers/dns/digitalocean" "github.com/xenolf/lego/providers/dns/dnsimple" + "github.com/xenolf/lego/providers/dns/dyn" "github.com/xenolf/lego/providers/dns/route53" ) @@ -31,6 +32,11 @@ type ProviderOpts struct { // DNSimple credentials DNSimpleEmail string DNSimpleKey string + + // Dyn credentials + DynCustomerName string + DynUserName string + DynPassword string } type DnsProvider string @@ -40,6 +46,7 @@ const ( DIGITALOCEAN = DnsProvider("DigitalOcean") ROUTE53 = DnsProvider("Route53") DNSIMPLE = DnsProvider("DNSimple") + DYN = DnsProvider("Dyn") ) var dnsProviderFactory = map[DnsProvider]interface{}{ @@ -47,6 +54,7 @@ var dnsProviderFactory = map[DnsProvider]interface{}{ DIGITALOCEAN: makeDigitalOceanProvider, ROUTE53: makeRoute53Provider, DNSIMPLE: makeDNSimpleProvider, + DYN: makeDynProvider, } func getProvider(opts ProviderOpts) (lego.ChallengeProvider, error) { @@ -115,7 +123,7 @@ func makeRoute53Provider(opts ProviderOpts) (lego.ChallengeProvider, error) { // returns a preconfigured DNSimple lego.ChallengeProvider func makeDNSimpleProvider(opts ProviderOpts) (lego.ChallengeProvider, error) { if len(opts.DNSimpleEmail) == 0 { - return nil, fmt.Errorf("DNSimple Email not set") + return nil, fmt.Errorf("DNSimple Email is not set") } if len(opts.DNSimpleKey) == 0 { return nil, fmt.Errorf("DNSimple API key is not set") @@ -127,3 +135,23 @@ func makeDNSimpleProvider(opts ProviderOpts) (lego.ChallengeProvider, error) { } return provider, nil } + +// returns a preconfigured Dyn lego.ChallengeProvider +func makeDynProvider(opts ProviderOpts) (lego.ChallengeProvider, error) { + if len(opts.DynCustomerName) == 0 { + return nil, fmt.Errorf("Dyn customer name is not set") + } + if len(opts.DynUserName) == 0 { + return nil, fmt.Errorf("Dyn user name is not set") + } + if len(opts.DynPassword) == 0 { + return nil, fmt.Errorf("Dyn password is not set") + } + + provider, err := dyn.NewDNSProviderCredentials(opts.DynCustomerName, + opts.DynUserName, opts.DynPassword) + if err != nil { + return nil, err + } + return provider, nil +}