func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) { zoneID, err := c.getHostedZoneID(fqdn) if err != nil { return nil, err } result, err := c.makeRequest( "GET", fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil, ) if err != nil { return nil, err } var records []cloudFlareRecord err = json.Unmarshal(result, &records) if err != nil { return nil, err } for _, rec := range records { if rec.Name == acme.UnFqdn(fqdn) { return &rec, nil } } return nil, fmt.Errorf("No existing record found for %s", fqdn) }
// Extract DNS zone and DNS entry name func (c *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) { zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return "", "", err } zone = acme.UnFqdn(zone) name := acme.UnFqdn(fqdn) name = name[:len(name)-len("."+zone)] return zone, name, nil }
func (c *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) { var zone hostedZone authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return nil, err } url := "/servers/localhost/zones" result, err := c.makeRequest("GET", url, nil) if err != nil { return nil, err } zones := []hostedZone{} err = json.Unmarshal(result, &zones) if err != nil { return nil, err } url = "" for _, zone := range zones { if acme.UnFqdn(zone.Name) == acme.UnFqdn(authZone) { url = zone.URL } } result, err = c.makeRequest("GET", url, nil) if err != nil { return nil, err } err = json.Unmarshal(result, &zone) if err != nil { return nil, err } // convert pre-v1 API result if len(zone.Records) > 0 { zone.RRSets = []rrSet{} for _, record := range zone.Records { set := rrSet{ Name: record.Name, Type: record.Type, Records: []pdnsRecord{record}, } zone.RRSets = append(zone.RRSets, set) } } return &zone, nil }
// Present creates a TXT record to fulfil the dns-01 challenge func (c *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, _ := acme.DNS01Record(domain, keyAuth) zoneID, err := c.getHostedZoneID(fqdn) if err != nil { return err } rec := cloudFlareRecord{ Type: "TXT", Name: acme.UnFqdn(fqdn), Content: value, TTL: 120, } body, err := json.Marshal(rec) if err != nil { return err } _, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body)) 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 { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[fqdn] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("unknown record ID for '%s'", fqdn) } authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) } authZone = acme.UnFqdn(authZone) reqURL := fmt.Sprintf("/domain/zone/%s/record/%d", authZone, recordID) err = d.client.Delete(reqURL, nil) if err != nil { fmt.Printf("Error when call OVH api to delete challenge record : %q \n", err) return err } // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, fqdn) d.recordIDsMu.Unlock() return nil }
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { // HostedZone represents a CloudFlare DNS zone type HostedZone struct { ID string `json:"id"` Name string `json:"name"` } authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return "", err } result, err := c.makeRequest("GET", "/zones?name="+acme.UnFqdn(authZone), nil) if err != nil { return "", err } var hostedZone []HostedZone err = json.Unmarshal(result, &hostedZone) if err != nil { return "", err } if len(hostedZone) != 1 { return "", fmt.Errorf("Zone %s not found in CloudFlare for domain %s", authZone, fqdn) } return hostedZone[0].ID, nil }
func (c *DNSProvider) extractRecordName(fqdn, domain string) string { name := acme.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } return name }
func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) { authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return "", err } // .DNSName should not have a trailing dot reqParams := &route53.ListHostedZonesByNameInput{ DNSName: aws.String(acme.UnFqdn(authZone)), } resp, err := r.client.ListHostedZonesByName(reqParams) if err != nil { return "", err } var hostedZoneID string for _, hostedZone := range resp.HostedZones { // .Name has a trailing dot if !*hostedZone.Config.PrivateZone && *hostedZone.Name == authZone { hostedZoneID = *hostedZone.Id break } } if len(hostedZoneID) == 0 { return "", fmt.Errorf("Zone %s not found in Route 53 for domain %s", authZone, fqdn) } if strings.HasPrefix(hostedZoneID, "/hostedzone/") { hostedZoneID = strings.TrimPrefix(hostedZoneID, "/hostedzone/") } return hostedZoneID, nil }
func (c *DNSProvider) getHostedZone(domain string) (string, string, error) { zones, _, err := c.client.Domains.List() if err != nil { return "", "", fmt.Errorf("dnspod API call failed: %v", err) } authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return "", "", err } var hostedZone dnspod.Domain for _, zone := range zones { if zone.Name == acme.UnFqdn(authZone) { hostedZone = zone } } if hostedZone.ID == 0 { return "", "", fmt.Errorf("Zone %s not found in dnspod for domain %s", authZone, domain) } return fmt.Sprintf("%v", hostedZone.ID), hostedZone.Name, nil }
// Present creates a TXT record to fulfil the dns-01 challenge func (c *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, _ := acme.DNS01Record(domain, keyAuth) zoneID, err := c.getHostedZoneID(fqdn) if err != nil { return err } rec := RackspaceRecords{ RackspaceRecord: []RackspaceRecord{{ Name: acme.UnFqdn(fqdn), Type: "TXT", Data: value, TTL: 300, }}, } body, err := json.Marshal(rec) if err != nil { return err } _, err = c.makeRequest("POST", fmt.Sprintf("/domains/%d/records", zoneID), bytes.NewReader(body)) if err != nil { return err } return nil }
// getHostedZoneID performs a lookup to get the DNS zone which needs // modifying for a given FQDN func (c *DNSProvider) getHostedZoneID(fqdn string) (int, error) { // HostedZones represents the response when querying Rackspace DNS zones type ZoneSearchResponse struct { TotalEntries int `json:"totalEntries"` HostedZones []struct { ID int `json:"id"` Name string `json:"name"` } `json:"domains"` } authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return 0, err } result, err := c.makeRequest("GET", fmt.Sprintf("/domains?name=%s", acme.UnFqdn(authZone)), nil) if err != nil { return 0, err } var zoneSearchResponse ZoneSearchResponse err = json.Unmarshal(result, &zoneSearchResponse) if err != nil { return 0, err } // 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 { return 0, fmt.Errorf("Found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn) } return zoneSearchResponse.HostedZones[0].ID, nil }
// CleanUp removes a given record that was generated by Present func (provider *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) provider.recordIDsMu.Lock() recordID, ok := provider.recordIDs[fqdn] provider.recordIDsMu.Unlock() if !ok { return fmt.Errorf("Unknown recordID for '%s'", fqdn) } authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) } authZone = acme.UnFqdn(authZone) zoneRecord, err := provider.getZoneInformationByName(authZone) if err != nil { return err } _, err = provider.client.RemoveRecord(zoneRecord.ID, recordID) if err != nil { return err } provider.recordIDsMu.Lock() delete(provider.recordIDs, fqdn) provider.recordIDsMu.Unlock() return nil }
// Present creates a TXT record to fulfil the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { // txtRecordRequest represents the request body to DO's API to make a TXT record type txtRecordRequest struct { FieldType string `json:"fieldType"` SubDomain string `json:"subDomain"` Target string `json:"target"` TTL int `json:"ttl"` } // txtRecordResponse represents a response from DO's API after making a TXT record type txtRecordResponse struct { ID int `json:"id"` FieldType string `json:"fieldType"` SubDomain string `json:"subDomain"` Target string `json:"target"` TTL int `json:"ttl"` Zone string `json:"zone"` } fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) // Parse domain name authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) } authZone = acme.UnFqdn(authZone) subDomain := d.extractRecordName(fqdn, authZone) reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone) reqData := txtRecordRequest{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: ttl} var respData txtRecordResponse // Create TXT record err = d.client.Post(reqURL, reqData, &respData) if err != nil { fmt.Printf("Error when call OVH api to add record : %q \n", err) return err } // Apply the change reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) err = d.client.Post(reqURL, nil, nil) if err != nil { fmt.Printf("Error when call OVH api to refresh zone : %q \n", err) return err } d.recordIDsMu.Lock() d.recordIDs[fqdn] = respData.ID d.recordIDsMu.Unlock() return nil }
// CleanUp removes the TXT record matching the specified parameters. func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) zone, err := c.getHostedZone(domain) if err != nil { return err } name := acme.UnFqdn(fqdn) _, err = c.client.Records.Delete(zone.Zone, name, "TXT") return err }
// Present creates a TXT record using the specified parameters. func (p *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, _ := acme.DNS01Record(domain, keyAuth) zone, err := p.getHostedZoneInfo(fqdn) if err != nil { return err } if _, err = p.linode.CreateDomainResourceTXT(zone.domainId, acme.UnFqdn(fqdn), value, 60); err != nil { return err } return nil }
func (c *DNSProvider) newTxtRecord(zone *dns.Zone, fqdn, value string, ttl int) *dns.Record { name := acme.UnFqdn(fqdn) return &dns.Record{ Type: "TXT", Zone: zone.Zone, Domain: name, TTL: ttl, Answers: []*dns.Answer{ {Rdata: []string{value}}, }, } }
// Present creates a TXT record to fulfil the dns-01 challenge func (c *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, _ := acme.DNS01Record(domain, keyAuth) zone, err := c.getHostedZone(fqdn) if err != nil { return err } name := fqdn // pre-v1 API wants non-fqdn if c.apiVersion == 0 { name = acme.UnFqdn(fqdn) } rec := pdnsRecord{ Content: "\"" + value + "\"", Disabled: false, // pre-v1 API Type: "TXT", Name: name, TTL: 120, } rrsets := rrSets{ RRSets: []rrSet{ rrSet{ Name: name, ChangeType: "REPLACE", Type: "TXT", Kind: "Master", TTL: 120, Records: []pdnsRecord{rec}, }, }, } body, err := json.Marshal(rrsets) if err != nil { return err } _, err = c.makeRequest("PATCH", zone.URL, bytes.NewReader(body)) if err != nil { fmt.Println("here") return err } return nil }
// CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { fqdn, _, _ := acme.DNS01Record(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() recordID, ok := d.recordIDs[fqdn] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("unknown record ID for '%s'", fqdn) } authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) } authZone = acme.UnFqdn(authZone) reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", digitalOceanBaseURL, authZone, recordID) req, err := http.NewRequest("DELETE", reqURL, nil) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken)) client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { var errInfo digitalOceanAPIError json.NewDecoder(resp.Body).Decode(&errInfo) return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message) } // Delete record ID from map d.recordIDsMu.Lock() delete(d.recordIDs, fqdn) d.recordIDsMu.Unlock() return nil }
// Checks that azure has a zone for this domain name. func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) { authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return "", err } // Now we want to to Azure and get the zone. dc := dns.NewZonesClient(c.subscriptionId) dc.Authorizer, err = c.newServicePrincipalTokenFromCredentials(azure.PublicCloud.ResourceManagerEndpoint) zone, err := dc.Get(c.resourceGroup, acme.UnFqdn(authZone)) if err != nil { return "", err } // zone.Name shouldn't have a trailing dot(.) return to.String(zone.Name), nil }
func (c *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) { zone, err := c.getHostedZone(fqdn) if err != nil { return nil, err } _, err = c.makeRequest("GET", zone.URL, nil) if err != nil { return nil, err } for _, set := range zone.RRSets { if (set.Name == acme.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" { return &set, nil } } return nil, fmt.Errorf("No existing record found for %s", fqdn) }
func (p *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) { // Lookup the zone that handles the specified FQDN. authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return nil, err } resourceName := strings.TrimSuffix(fqdn, "."+authZone) // Query the authority zone. domain, err := p.linode.GetDomain(acme.UnFqdn(authZone)) if err != nil { return nil, err } return &hostedZoneInfo{ domainId: domain.DomainID, resourceName: resourceName, }, nil }
// Present creates a record with a secret func (provider *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, _ := acme.DNS01Record(domain, keyAuth) authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) } // 1. Aurora will happily create the TXT record when it is provided a fqdn, // but it will only appear in the control panel and will not be // propagated to DNS servers. Extract and use subdomain instead. // 2. A trailing dot in the fqdn will cause Aurora to add a trailing dot to // the subdomain, resulting in _acme-challenge..<domain> rather // than _acme-challenge.<domain> subdomain := fqdn[0 : len(fqdn)-len(authZone)-1] authZone = acme.UnFqdn(authZone) zoneRecord, err := provider.getZoneInformationByName(authZone) reqData := records.CreateRecordRequest{ RecordType: "TXT", Name: subdomain, Content: value, TTL: 300, } respData, err := provider.client.CreateRecord(zoneRecord.ID, reqData) if err != nil { return fmt.Errorf("Could not create record: '%s'.", err) } provider.recordIDsMu.Lock() provider.recordIDs[fqdn] = respData.ID provider.recordIDsMu.Unlock() return nil }
// newChallenge builds a challenge record from a domain name, a challenge // authentication key, and a map of available TLDs. func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, error) { domain = acme.UnFqdn(domain) parts := strings.Split(domain, ".") // Find the longest matching TLD. longest := -1 for i := len(parts); i > 0; i-- { t := strings.Join(parts[i-1:], ".") if _, found := tlds[t]; found { longest = i - 1 } } if longest < 1 { return nil, fmt.Errorf("Invalid domain name '%s'", domain) } tld := strings.Join(parts[longest:], ".") sld := parts[longest-1] var host string if longest >= 1 { host = strings.Join(parts[:longest-1], ".") } key, keyValue, _ := acme.DNS01Record(domain, keyAuth) return &challenge{ domain: domain, key: "_acme-challenge." + host, keyFqdn: key, keyValue: keyValue, tld: tld, sld: sld, host: host, }, nil }
// findTxtRecord searches a DNS zone for a TXT record with a specific name func (c *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*RackspaceRecord, error) { result, err := c.makeRequest("GET", fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil) if err != nil { return nil, err } var records RackspaceRecords err = json.Unmarshal(result, &records) if err != nil { return nil, err } recordsLength := len(records.RackspaceRecord) switch recordsLength { case 1: break case 0: return nil, fmt.Errorf("No TXT record found for %s", fqdn) default: return nil, fmt.Errorf("More than 1 TXT record found for %s", fqdn) } return &records.RackspaceRecord[0], nil }
// Returns the relative record to the domain func toRelativeRecord(domain, zone string) string { return acme.UnFqdn(strings.TrimSuffix(domain, zone)) }
// Present creates a TXT record using the specified parameters. It // does this by creating and activating a new temporary Gandi DNS // zone. This new zone contains the TXT record. func (d *DNSProvider) Present(domain, token, keyAuth string) error { fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) if ttl < 300 { ttl = 300 // 300 is gandi minimum value for ttl } // find authZone and Gandi zone_id for fqdn authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Gandi DNS: findZoneByFqdn failure: %v", err) } zoneID, err := d.getZoneID(authZone) if err != nil { return err } // determine name of TXT record if !strings.HasSuffix( strings.ToLower(fqdn), strings.ToLower("."+authZone)) { return fmt.Errorf( "Gandi DNS: unexpected authZone %s for fqdn %s", authZone, fqdn) } name := fqdn[:len(fqdn)-len("."+authZone)] // acquire lock and check there is not a challenge already in // progress for this value of authZone d.inProgressMu.Lock() defer d.inProgressMu.Unlock() if _, ok := d.inProgressAuthZones[authZone]; ok { return fmt.Errorf( "Gandi DNS: challenge already in progress for authZone %s", authZone) } // perform API actions to create and activate new gandi zone // containing the required TXT record newZoneName := fmt.Sprintf( "%s [ACME Challenge %s]", acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z)) newZoneID, err := d.cloneZone(zoneID, newZoneName) if err != nil { return err } newZoneVersion, err := d.newZoneVersion(newZoneID) if err != nil { return err } err = d.addTXTRecord(newZoneID, newZoneVersion, name, value, ttl) if err != nil { return err } err = d.setZoneVersion(newZoneID, newZoneVersion) if err != nil { return err } err = d.setZone(authZone, newZoneID) if err != nil { return err } // save data necessary for CleanUp d.inProgressFQDNs[fqdn] = inProgressInfo{ zoneID: zoneID, newZoneID: newZoneID, authZone: authZone, } d.inProgressAuthZones[authZone] = struct{}{} return nil }
// Present creates a TXT record using the specified parameters 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) authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) if err != nil { return fmt.Errorf("Could not determine zone for domain: '%s'. %s", domain, err) } authZone = acme.UnFqdn(authZone) reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, authZone) reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value} body, err := json.Marshal(reqData) if err != nil { return err } req, err := http.NewRequest("POST", reqURL, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.apiAuthToken)) client := http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { var errInfo digitalOceanAPIError json.NewDecoder(resp.Body).Decode(&errInfo) return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message) } // Everything looks good; but we'll need the ID later to delete the record var respData txtRecordResponse err = json.NewDecoder(resp.Body).Decode(&respData) if err != nil { return err } d.recordIDsMu.Lock() d.recordIDs[fqdn] = respData.DomainRecord.ID d.recordIDsMu.Unlock() return nil }