libdns-gandi

Fork https://github.com/foxcpp/libdns-gandi

git clone git://git.lin.moe/go/libdns-gandi.git

  1package gandi
  2
  3import (
  4	"bytes"
  5	"context"
  6	"encoding/json"
  7	"fmt"
  8	"net/http"
  9	"strings"
 10
 11	"github.com/libdns/libdns"
 12)
 13
 14func (p *Provider) setRecord(ctx context.Context, zone string, rr libdns.RR, domain gandiDomain) error {
 15	p.mutex.Lock()
 16	defer p.mutex.Unlock()
 17
 18	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", domain.DomainRecordsHref, rr.Name, rr.Type), nil)
 19	if err != nil {
 20		return err
 21	}
 22
 23	var oldGandiRecord gandiRecord
 24	status, err := p.doRequest(req, &oldGandiRecord)
 25
 26	// ignore if no record is found, then we can create one safely
 27	if status.Code != http.StatusNotFound && err != nil {
 28		return err
 29	}
 30
 31	// we check if the new value does not already exists and append it if not
 32	var exists bool
 33	for _, val := range oldGandiRecord.RRSetValues {
 34		if val == rr.Data {
 35			exists = true
 36			break
 37		}
 38	}
 39
 40	recValues := oldGandiRecord.RRSetValues
 41	if !exists {
 42		recValues = append(recValues, rr.Data)
 43	}
 44
 45	// we just create a new record, if an existing record was found, we just append the new value to the existing ones
 46	newGandiRecord := gandiRecord{
 47		RRSetTTL:    int(rr.TTL.Seconds()),
 48		RRSetType:   rr.Type,
 49		RRSetName:   rr.Name,
 50		RRSetValues: recValues,
 51	}
 52
 53	raw, err := json.Marshal(newGandiRecord)
 54	if err != nil {
 55		return err
 56	}
 57
 58	// we update existing record or create a new record if it does not exist yet
 59	req, err = http.NewRequestWithContext(ctx, "PUT", fmt.Sprintf("%s/%s/%s", domain.DomainRecordsHref, rr.Name, rr.Type), bytes.NewReader(raw))
 60	if err != nil {
 61		return err
 62	}
 63
 64	req.Header.Set("Content-Type", "application/json")
 65
 66	_, err = p.doRequest(req, nil)
 67
 68	return err
 69}
 70
 71func (p *Provider) deleteRecord(ctx context.Context, zone string, rr libdns.RR, domain gandiDomain) error {
 72	p.mutex.Lock()
 73	defer p.mutex.Unlock()
 74
 75	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", domain.DomainRecordsHref, rr.Name, rr.Type), nil)
 76	if err != nil {
 77		return err
 78	}
 79
 80	// check if the record exists beforehand
 81	var rec gandiRecord
 82	_, err = p.doRequest(req, &rec)
 83	if err != nil {
 84		return err
 85	}
 86
 87	// might contain request crafting error
 88	var requestErr error
 89
 90	if len(rec.RRSetValues) > 1 {
 91		// if it contains multiple values, the best is to update the record instead of deleting all the values
 92		newRRSetValues := []string{}
 93		for _, val := range rec.RRSetValues {
 94			if val != rr.Data {
 95				newRRSetValues = append(newRRSetValues, val)
 96			}
 97		}
 98		rec.RRSetValues = newRRSetValues
 99
100		raw, err := json.Marshal(rec)
101		if err != nil {
102			return err
103		}
104
105		req, requestErr = http.NewRequestWithContext(ctx, "PUT", fmt.Sprintf("%s/%s/%s", domain.DomainRecordsHref, rr.Name, rr.Type), bytes.NewReader(raw))
106	} else {
107		// if there is only one entry, we make sure that the value to delete is matching the one we found
108		// otherwise we may delete the wrong record
109		if strings.Trim(rec.RRSetValues[0], "\"") != rr.Data {
110			return fmt.Errorf("LiveDNS returned a %v (%v)", http.StatusNotFound, "Can't find such a DNS value")
111		}
112
113		req, requestErr = http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/%s/%s", domain.DomainRecordsHref, rr.Name, rr.Type), nil)
114	}
115
116	// we check if NewRequestWithContext threw an error
117	if requestErr != nil {
118		return requestErr
119	}
120
121	req.Header.Set("Content-Type", "application/json")
122
123	_, requestErr = p.doRequest(req, nil)
124	return requestErr
125}
126
127func (p *Provider) getDomain(ctx context.Context, zone string) (gandiDomain, error) {
128	p.mutex.Lock()
129	defer p.mutex.Unlock()
130
131	// we trim the dot at the end of the zone name to get the fqdn
132	// and then use it to fetch domain information through the api
133	fqdn := strings.TrimRight(zone, ".")
134
135	if p.domains == nil {
136		p.domains = make(map[string]gandiDomain)
137	}
138	if domain, ok := p.domains[fqdn]; ok {
139		return domain, nil
140	}
141
142	// might contain request crafting error
143	var requestErr error
144
145	req, requestErr := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://api.gandi.net/v5/livedns/domains/%s", fqdn), nil)
146	if requestErr != nil {
147		return gandiDomain{}, requestErr
148	}
149
150	var domain gandiDomain
151
152	if _, requestErr = p.doRequest(req, &domain); requestErr != nil {
153		return gandiDomain{}, requestErr
154	}
155
156	p.domains[fqdn] = domain
157
158	return domain, nil
159}
160
161func (p *Provider) doRequest(req *http.Request, result interface{}) (gandiStatus, error) {
162	if p.BearerToken != "" {
163		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.BearerToken))
164	} else if p.APIToken != "" {
165		req.Header.Set("Authorization", fmt.Sprintf("Apikey %s", p.APIToken))
166	}
167	req.Header.Set("Accept", "application/json")
168
169	resp, err := http.DefaultClient.Do(req)
170
171	if err != nil {
172		return gandiStatus{}, err
173	}
174
175	defer resp.Body.Close()
176
177	if resp.StatusCode >= 400 {
178		var response gandiStatus
179		if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
180			return gandiStatus{}, err
181		}
182
183		return response, fmt.Errorf("LiveDNS returned a %v (%v)", response.Code, response.Message)
184	}
185
186	// the api does not return the json object on 201 or 204, so we just stop here
187	if resp.StatusCode > 200 {
188		return gandiStatus{}, nil
189	}
190
191	// if we get a 200, we parse the json object
192	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
193		return gandiStatus{}, err
194	}
195
196	return gandiStatus{}, nil
197}