diff --git a/Gopkg.lock b/Gopkg.lock index 355b7c43..bd751230 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -34,6 +34,12 @@ revision = "fa1c0367800db75e4d10d0ec90c49a8731670224" version = "1.15.0" +[[projects]] + branch = "master" + name = "github.com/OpenDNS/vegadns2client" + packages = ["."] + revision = "a3fa4a771d87bda2514a90a157e1fed1b6897d2e" + [[projects]] name = "github.com/akamai/AkamaiOPEN-edgegrid-golang" packages = [ @@ -374,6 +380,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "d93e6f0825f1f7c1c3c2ccd421af4d99b77b390f3346ca751968af0609119c96" + inputs-digest = "68128f1cf61f649cdac1483e18c54541782cbd344605cab3cf6c2b448a86bda0" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cli.go b/cli.go index a499dc76..0dcc3113 100644 --- a/cli.go +++ b/cli.go @@ -221,6 +221,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER") fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID") fmt.Fprintln(w, "\tdyn:\tDYN_CUSTOMER_NAME, DYN_USER_NAME, DYN_PASSWORD") + fmt.Fprintln(w, "\tvegadns:\tSECRET_VEGADNS_KEY, SECRET_VEGADNS_SECRET, VEGADNS_URL") fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY") fmt.Fprintln(w, "\tovh:\tOVH_ENDPOINT, OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY") fmt.Fprintln(w, "\tpdns:\tPDNS_API_KEY, PDNS_API_URL") diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 2a4c80e3..6e66b983 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -35,6 +35,7 @@ import ( "github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/route53" "github.com/xenolf/lego/providers/dns/sakuracloud" + "github.com/xenolf/lego/providers/dns/vegadns" "github.com/xenolf/lego/providers/dns/vultr" ) @@ -107,6 +108,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) return otc.NewDNSProvider() case "exec": return exec.NewDNSProvider() + case "vegadns": + return vegadns.NewDNSProvider() default: return nil, fmt.Errorf("unrecognised DNS provider: %s", name) } diff --git a/providers/dns/vegadns/vegadns.go b/providers/dns/vegadns/vegadns.go new file mode 100644 index 00000000..4d5371b8 --- /dev/null +++ b/providers/dns/vegadns/vegadns.go @@ -0,0 +1,85 @@ +// Package vegadns implements a DNS provider for solving the DNS-01 +// challenge using VegaDNS. +package vegadns + +import ( + "fmt" + "os" + "strings" + "time" + + vegaClient "github.com/OpenDNS/vegadns2client" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/config/env" +) + +// DNSProvider describes a provider for VegaDNS +type DNSProvider struct { + client vegaClient.VegaDNSClient +} + +// NewDNSProvider returns a DNSProvider instance configured for VegaDNS. +// Credentials must be passed in the environment variables: +// VEGADNS_URL, SECRET_VEGADNS_KEY, SECRET_VEGADNS_SECRET. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get("VEGADNS_URL") + if err != nil { + return nil, fmt.Errorf("VegaDNS: %v", err) + } + + key := os.Getenv("SECRET_VEGADNS_KEY") + secret := os.Getenv("SECRET_VEGADNS_SECRET") + + return NewDNSProviderCredentials(values["VEGADNS_URL"], key, secret) +} + +// NewDNSProviderCredentials uses the supplied credentials to return a +// DNSProvider instance configured for VegaDNS. +func NewDNSProviderCredentials(vegaDNSURL string, key string, secret string) (*DNSProvider, error) { + vega := vegaClient.NewVegaDNSClient(vegaDNSURL) + vega.APIKey = key + vega.APISecret = secret + + return &DNSProvider{ + client: vega, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS +// propagation. Adjusting here to cope with spikes in propagation times. +func (r *DNSProvider) Timeout() (timeout, interval time.Duration) { + timeout = 12 * time.Minute + interval = 1 * time.Minute + return +} + +// Present creates a TXT record to fulfil the dns-01 challenge +func (r *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + + _, domainID, err := r.client.GetAuthZone(fqdn) + if err != nil { + return fmt.Errorf("can't find Authoritative Zone for %s in Present: %v", fqdn, err) + } + + return r.client.CreateTXT(domainID, fqdn, value, 10) +} + +// CleanUp removes the TXT record matching the specified parameters +func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + + _, domainID, err := r.client.GetAuthZone(fqdn) + if err != nil { + return fmt.Errorf("can't find Authoritative Zone for %s in CleanUp: %v", fqdn, err) + } + + txt := strings.TrimSuffix(fqdn, ".") + + recordID, err := r.client.GetRecordID(domainID, txt, "TXT") + if err != nil { + return fmt.Errorf("couldn't get Record ID in CleanUp: %s", err) + } + + return r.client.DeleteRecord(recordID) +} diff --git a/providers/dns/vegadns/vegadns_test.go b/providers/dns/vegadns/vegadns_test.go new file mode 100644 index 00000000..154771e8 --- /dev/null +++ b/providers/dns/vegadns/vegadns_test.go @@ -0,0 +1,391 @@ +// Package vegadns implements a DNS provider for solving the DNS-01 +// challenge using VegaDNS. +package vegadns + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ipPort = "127.0.0.1:2112" + +var jsonMap = map[string]string{ + "token": ` +{ + "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", + "token_type":"bearer", + "expires_in":3600 +} +`, + "domains": ` +{ + "domains":[ + { + "domain_id":1, + "domain":"example.com", + "status":"active", + "owner_id":0 + } + ] +} +`, + "records": ` +{ + "status":"ok", + "total_records":2, + "domain":{ + "status":"active", + "domain":"example.com", + "owner_id":0, + "domain_id":1 + }, + "records":[ + { + "retry":"2048", + "minimum":"2560", + "refresh":"16384", + "email":"hostmaster.example.com", + "record_type":"SOA", + "expire":"1048576", + "ttl":86400, + "record_id":1, + "nameserver":"ns1.example.com", + "domain_id":1, + "serial":"" + }, + { + "name":"example.com", + "value":"ns1.example.com", + "record_type":"NS", + "ttl":3600, + "record_id":2, + "location_id":null, + "domain_id":1 + }, + { + "name":"_acme-challenge.example.com", + "value":"my_challenge", + "record_type":"TXT", + "ttl":3600, + "record_id":3, + "location_id":null, + "domain_id":1 + } + ] +} +`, + "recordCreated": ` +{ + "status":"ok", + "record":{ + "name":"_acme-challenge.example.com", + "value":"my_challenge", + "record_type":"TXT", + "ttl":3600, + "record_id":3, + "location_id":null, + "domain_id":1 + } +} +`, + "recordDeleted": `{"status": "ok"}`, +} + +type muxCallback func() *http.ServeMux + +func TestVegaDNSNewDNSProviderFail(t *testing.T) { + os.Setenv("VEGADNS_URL", "") + _, err := NewDNSProvider() + assert.Error(t, err, "VEGADNS_URL env missing") +} + +func TestVegaDNSTimeoutSuccess(t *testing.T) { + ts, err := startTestServer(vegaDNSMuxSuccess) + require.NoError(t, err) + + defer ts.Close() + defer os.Clearenv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + timeout, interval := provider.Timeout() + assert.Equal(t, timeout, time.Duration(720000000000)) + assert.Equal(t, interval, time.Duration(60000000000)) +} + +func TestVegaDNSPresentSuccess(t *testing.T) { + ts, err := startTestServer(vegaDNSMuxSuccess) + require.NoError(t, err) + + defer ts.Close() + defer os.Clearenv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present("example.com", "token", "keyAuth") + assert.NoError(t, err) +} + +func TestVegaDNSPresentFailToFindZone(t *testing.T) { + ts, err := startTestServer(vegaDNSMuxFailToFindZone) + require.NoError(t, err) + + defer ts.Close() + defer os.Clearenv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present("example.com", "token", "keyAuth") + assert.EqualError(t, err, "can't find Authoritative Zone for _acme-challenge.example.com. in Present: Unable to find auth zone for fqdn _acme-challenge.example.com") +} + +func TestVegaDNSPresentFailToCreateTXT(t *testing.T) { + ts, err := startTestServer(vegaDNSMuxFailToCreateTXT) + require.NoError(t, err) + + defer ts.Close() + defer os.Clearenv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present("example.com", "token", "keyAuth") + assert.EqualError(t, err, "Got bad answer from VegaDNS on CreateTXT. Code: 400. Message: ") +} + +func TestVegaDNSCleanUpSuccess(t *testing.T) { + ts, err := startTestServer(vegaDNSMuxSuccess) + require.NoError(t, err) + + defer ts.Close() + defer os.Clearenv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp("example.com", "token", "keyAuth") + assert.NoError(t, err) +} + +func TestVegaDNSCleanUpFailToFindZone(t *testing.T) { + ts, err := startTestServer(vegaDNSMuxFailToFindZone) + require.NoError(t, err) + + defer ts.Close() + defer os.Clearenv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp("example.com", "token", "keyAuth") + assert.EqualError(t, err, "can't find Authoritative Zone for _acme-challenge.example.com. in CleanUp: Unable to find auth zone for fqdn _acme-challenge.example.com") +} + +func TestVegaDNSCleanUpFailToGetRecordID(t *testing.T) { + ts, err := startTestServer(vegaDNSMuxFailToGetRecordID) + require.NoError(t, err) + + defer ts.Close() + defer os.Clearenv() + + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp("example.com", "token", "keyAuth") + assert.EqualError(t, err, "couldn't get Record ID in CleanUp: Got bad answer from VegaDNS on GetRecordID. Code: 404. Message: ") +} + +func vegaDNSMuxSuccess() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["token"]) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("search") == "example.com" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["domains"]) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + if r.URL.Query().Get("domain_id") == "1" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["records"]) + return + } + w.WriteHeader(http.StatusNotFound) + return + case http.MethodPost: + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, jsonMap["recordCreated"]) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/records/3", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["recordDeleted"]) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Printf("Not Found for Request: (%+v)\n\n", r) + }) + + return mux +} + +func vegaDNSMuxFailToFindZone() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["token"]) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + + return mux +} + +func vegaDNSMuxFailToCreateTXT() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["token"]) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("search") == "example.com" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["domains"]) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + if r.URL.Query().Get("domain_id") == "1" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["records"]) + return + } + w.WriteHeader(http.StatusNotFound) + return + case http.MethodPost: + w.WriteHeader(http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + return mux +} + +func vegaDNSMuxFailToGetRecordID() *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["token"]) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("search") == "example.com" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, jsonMap["domains"]) + return + } + w.WriteHeader(http.StatusNotFound) + }) + + mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusBadRequest) + }) + + return mux +} + +// Starts and returns a test server using a custom ip/port. Defer close() afterwards. +func startTestServer(callback muxCallback) (*httptest.Server, error) { + err := os.Setenv("SECRET_VEGADNS_KEY", "key") + if err != nil { + return nil, err + } + + err = os.Setenv("SECRET_VEGADNS_SECRET", "secret") + if err != nil { + return nil, err + } + + err = os.Setenv("VEGADNS_URL", "http://"+ipPort) + if err != nil { + return nil, err + } + + ts := httptest.NewUnstartedServer(callback()) + + l, err := net.Listen("tcp", ipPort) + if err != nil { + return nil, err + } + + ts.Listener = l + ts.Start() + + return ts, nil +} diff --git a/vendor/github.com/OpenDNS/vegadns2client/LICENSE b/vendor/github.com/OpenDNS/vegadns2client/LICENSE new file mode 100644 index 00000000..f9361e31 --- /dev/null +++ b/vendor/github.com/OpenDNS/vegadns2client/LICENSE @@ -0,0 +1,13 @@ +Copyright 2018, Cisco Systems, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/github.com/OpenDNS/vegadns2client/client.go b/vendor/github.com/OpenDNS/vegadns2client/client.go new file mode 100644 index 00000000..21096568 --- /dev/null +++ b/vendor/github.com/OpenDNS/vegadns2client/client.go @@ -0,0 +1,71 @@ +package vegadns2client + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +// VegaDNSClient - Struct for holding VegaDNSClient specific attributes +type VegaDNSClient struct { + client http.Client + baseurl string + version string + User string + Pass string + APIKey string + APISecret string + token Token +} + +// Send - Central place for sending requests +// Input: method, endpoint, params +// Output: *http.Response +func (vega *VegaDNSClient) Send(method string, endpoint string, params map[string]string) (*http.Response, error) { + vegaURL := vega.getURL(endpoint) + + p := url.Values{} + for k, v := range params { + p.Set(k, v) + } + + var err error + var req *http.Request + + if (method == "GET") || (method == "DELETE") { + vegaURL = fmt.Sprintf("%s?%s", vegaURL, p.Encode()) + req, err = http.NewRequest(method, vegaURL, nil) + } else { + req, err = http.NewRequest(method, vegaURL, strings.NewReader(p.Encode())) + } + + if err != nil { + return nil, fmt.Errorf("Error preparing request: %s", err) + } + + if vega.User != "" && vega.Pass != "" { + // Basic Auth + req.SetBasicAuth(vega.User, vega.Pass) + } else if vega.APIKey != "" && vega.APISecret != "" { + // OAuth + vega.getAuthToken() + err = vega.token.valid() + if err != nil { + return nil, err + } + req.Header.Set("Authorization", vega.getBearer()) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + return vega.client.Do(req) +} + +func (vega *VegaDNSClient) getURL(endpoint string) string { + return fmt.Sprintf("%s/%s/%s", vega.baseurl, vega.version, endpoint) +} + +func (vega *VegaDNSClient) stillAuthorized() error { + return vega.token.valid() +} diff --git a/vendor/github.com/OpenDNS/vegadns2client/domains.go b/vendor/github.com/OpenDNS/vegadns2client/domains.go new file mode 100644 index 00000000..b9713f4e --- /dev/null +++ b/vendor/github.com/OpenDNS/vegadns2client/domains.go @@ -0,0 +1,80 @@ +package vegadns2client + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" +) + +// Domain - struct containing a domain object +type Domain struct { + Status string `json:"status"` + Domain string `json:"domain"` + DomainID int `json:"domain_id"` + OwnerID int `json:"owner_id"` +} + +// DomainResponse - api response of a domain list +type DomainResponse struct { + Status string `json:"status"` + Total int `json:"total_domains"` + Domains []Domain `json:"domains"` +} + +// GetDomainID - returns the id for a domain +// Input: domain +// Output: int, err +func (vega *VegaDNSClient) GetDomainID(domain string) (int, error) { + params := make(map[string]string) + params["search"] = domain + + resp, err := vega.Send("GET", "domains", params) + + if err != nil { + return -1, fmt.Errorf("Error sending GET to GetDomainID: %s", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return -1, fmt.Errorf("Error reading response from GET to GetDomainID: %s", err) + } + if resp.StatusCode != http.StatusOK { + return -1, fmt.Errorf("Got bad answer from VegaDNS on GetDomainID. Code: %d. Message: %s", resp.StatusCode, string(body)) + } + + answer := DomainResponse{} + if err := json.Unmarshal(body, &answer); err != nil { + return -1, fmt.Errorf("Error unmarshalling body from GetDomainID: %s", err) + } + log.Println(answer) + for _, d := range answer.Domains { + if d.Domain == domain { + return d.DomainID, nil + } + } + + return -1, fmt.Errorf("Didnt find domain %s", domain) + +} + +// GetAuthZone retrieves the closest match to a given +// domain. Example: Given an argument "a.b.c.d.e", and a VegaDNS +// hosted domain of "c.d.e", GetClosestMatchingDomain will return +// "c.d.e". +func (vega *VegaDNSClient) GetAuthZone(fqdn string) (string, int, error) { + fqdn = strings.TrimSuffix(fqdn, ".") + numComponents := len(strings.Split(fqdn, ".")) + for i := 1; i < numComponents; i++ { + tmpHostname := strings.SplitN(fqdn, ".", i)[i-1] + log.Printf("tmpHostname for i = %d: %s\n", i, tmpHostname) + if domainID, err := vega.GetDomainID(tmpHostname); err == nil { + log.Printf("Found zone: %s\n\tShortened to %s\n", tmpHostname, strings.TrimSuffix(tmpHostname, ".")) + return strings.TrimSuffix(tmpHostname, "."), domainID, nil + } + } + log.Println("Unable to find hosted zone in vegadns") + return "", -1, fmt.Errorf("Unable to find auth zone for fqdn %s", fqdn) +} diff --git a/vendor/github.com/OpenDNS/vegadns2client/main.go b/vendor/github.com/OpenDNS/vegadns2client/main.go new file mode 100644 index 00000000..0d3e79a6 --- /dev/null +++ b/vendor/github.com/OpenDNS/vegadns2client/main.go @@ -0,0 +1,19 @@ +package vegadns2client + +import ( + "net/http" + "time" +) + +// NewVegaDNSClient - helper to instantiate a client +// Input: url string +// Output: VegaDNSClient +func NewVegaDNSClient(url string) VegaDNSClient { + httpClient := http.Client{Timeout: 15 * time.Second} + return VegaDNSClient{ + client: httpClient, + baseurl: url, + version: "1.0", + token: Token{}, + } +} diff --git a/vendor/github.com/OpenDNS/vegadns2client/records.go b/vendor/github.com/OpenDNS/vegadns2client/records.go new file mode 100644 index 00000000..36bd8d90 --- /dev/null +++ b/vendor/github.com/OpenDNS/vegadns2client/records.go @@ -0,0 +1,113 @@ +package vegadns2client + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +// Record - struct representing a Record object +type Record struct { + Name string `json:"name"` + Value string `json:"value"` + RecordType string `json:"record_type"` + TTL int `json:"ttl"` + RecordID int `json:"record_id"` + LocationID string `json:"location_id"` + DomainID int `json:"domain_id"` +} + +// RecordsResponse - api response list of records +type RecordsResponse struct { + Status string `json:"status"` + Total int `json:"total_records"` + Domain Domain `json:"domain"` + Records []Record `json:"records"` +} + +// GetRecordID - helper to get the id of a record +// Input: domainID, record, recordType +// Output: int +func (vega *VegaDNSClient) GetRecordID(domainID int, record string, recordType string) (int, error) { + params := make(map[string]string) + params["domain_id"] = fmt.Sprintf("%d", domainID) + + resp, err := vega.Send("GET", "records", params) + + if err != nil { + return -1, fmt.Errorf("Error sending GET to GetRecordID: %s", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return -1, fmt.Errorf("Error reading response from GetRecordID: %s", err) + } + if resp.StatusCode != http.StatusOK { + return -1, fmt.Errorf("Got bad answer from VegaDNS on GetRecordID. Code: %d. Message: %s", resp.StatusCode, string(body)) + } + + answer := RecordsResponse{} + if err := json.Unmarshal(body, &answer); err != nil { + return -1, fmt.Errorf("Error unmarshalling body from GetRecordID: %s", err) + } + + for _, r := range answer.Records { + if r.Name == record && r.RecordType == recordType { + return r.RecordID, nil + } + } + + return -1, errors.New("Couldnt find record") +} + +// CreateTXT - Creates a TXT record +// Input: domainID, fqdn, value, ttl +// Output: nil or error +func (vega *VegaDNSClient) CreateTXT(domainID int, fqdn string, value string, ttl int) error { + params := make(map[string]string) + + params["record_type"] = "TXT" + params["ttl"] = fmt.Sprintf("%d", ttl) + params["domain_id"] = fmt.Sprintf("%d", domainID) + params["name"] = strings.TrimSuffix(fqdn, ".") + params["value"] = value + + resp, err := vega.Send("POST", "records", params) + + if err != nil { + return fmt.Errorf("Send POST error in CreateTXT: %s", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("Error reading POST response in CreateTXT: %s", err) + } + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("Got bad answer from VegaDNS on CreateTXT. Code: %d. Message: %s", resp.StatusCode, string(body)) + } + + return nil +} + +// DeleteRecord - deletes a record by id +// Input: recordID +// Output: nil or error +func (vega *VegaDNSClient) DeleteRecord(recordID int) error { + resp, err := vega.Send("DELETE", fmt.Sprintf("records/%d", recordID), nil) + if err != nil { + return fmt.Errorf("Send DELETE error in DeleteTXT: %s", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("Error reading DELETE response in DeleteTXT: %s", err) + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Got bad answer from VegaDNS on DeleteTXT. Code: %d. Message: %s", resp.StatusCode, string(body)) + } + + return nil +} diff --git a/vendor/github.com/OpenDNS/vegadns2client/tokens.go b/vendor/github.com/OpenDNS/vegadns2client/tokens.go new file mode 100644 index 00000000..9e7706b0 --- /dev/null +++ b/vendor/github.com/OpenDNS/vegadns2client/tokens.go @@ -0,0 +1,74 @@ +package vegadns2client + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + "time" +) + +// Token - struct to hold token information +type Token struct { + Token string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + ExpiresAt time.Time +} + +func (t Token) valid() error { + if time.Now().UTC().After(t.ExpiresAt) { + return errors.New("Token Expired") + } + return nil +} + +func (vega *VegaDNSClient) getBearer() string { + if vega.token.valid() != nil { + vega.getAuthToken() + } + return vega.token.formatBearer() +} + +func (t Token) formatBearer() string { + return fmt.Sprintf("Bearer %s", t.Token) +} + +func (vega *VegaDNSClient) getAuthToken() { + tokenEndpoint := vega.getURL("token") + v := url.Values{} + v.Set("grant_type", "client_credentials") + + req, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(v.Encode())) + if err != nil { + log.Fatalf("Error forming POST to getAuthToken: %s", err) + } + req.SetBasicAuth(vega.APIKey, vega.APISecret) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + issueTime := time.Now().UTC() + resp, err := vega.client.Do(req) + if err != nil { + log.Fatalf("Error sending POST to getAuthToken: %s", err) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response from POST to getAuthToken: %s", err) + } + if resp.StatusCode != http.StatusOK { + log.Fatalf("Got bad answer from VegaDNS on getAuthToken. Code: %d. Message: %s", resp.StatusCode, string(body)) + } + if err := json.Unmarshal(body, &vega.token); err != nil { + log.Fatalf("Error unmarshalling body of POST to getAuthToken: %s", err) + } + + if vega.token.TokenType != "bearer" { + log.Fatal("We don't support anything except bearer tokens") + } + vega.token.ExpiresAt = issueTime.Add(time.Duration(vega.token.ExpiresIn) * time.Second) +}