2019-03-11 18:56:48 +02:00
|
|
|
package stackpath
|
2018-10-09 21:58:32 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2020-02-27 20:14:46 +02:00
|
|
|
"errors"
|
2018-10-09 21:58:32 +02:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"path"
|
|
|
|
|
2020-09-02 03:20:01 +02:00
|
|
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
2018-10-09 21:58:32 +02:00
|
|
|
"golang.org/x/net/publicsuffix"
|
|
|
|
)
|
|
|
|
|
2020-05-08 19:35:25 +02:00
|
|
|
// Zones is the response struct from the Stackpath api GetZones.
|
2018-10-09 21:58:32 +02:00
|
|
|
type Zones struct {
|
|
|
|
Zones []Zone `json:"zones"`
|
|
|
|
}
|
|
|
|
|
2020-05-08 19:35:25 +02:00
|
|
|
// Zone a DNS zone representation.
|
2018-10-09 21:58:32 +02:00
|
|
|
type Zone struct {
|
|
|
|
ID string
|
|
|
|
Domain string
|
|
|
|
}
|
|
|
|
|
2020-05-08 19:35:25 +02:00
|
|
|
// Records is the response struct from the Stackpath api GetZoneRecords.
|
2018-10-09 21:58:32 +02:00
|
|
|
type Records struct {
|
|
|
|
Records []Record `json:"records"`
|
|
|
|
}
|
|
|
|
|
2020-05-08 19:35:25 +02:00
|
|
|
// Record a DNS record representation.
|
2018-10-09 21:58:32 +02:00
|
|
|
type Record struct {
|
|
|
|
ID string `json:"id,omitempty"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
Type string `json:"type"`
|
|
|
|
TTL int `json:"ttl"`
|
|
|
|
Data string `json:"data"`
|
|
|
|
}
|
|
|
|
|
2020-05-08 19:35:25 +02:00
|
|
|
// ErrorResponse the API error response representation.
|
2018-10-09 21:58:32 +02:00
|
|
|
type ErrorResponse struct {
|
|
|
|
Code int `json:"code"`
|
|
|
|
Message string `json:"error"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *ErrorResponse) Error() string {
|
|
|
|
return fmt.Sprintf("%d %s", e.Code, e.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.stackpath.com/en/api/dns/#operation/GetZones
|
|
|
|
func (d *DNSProvider) getZones(domain string) (*Zone, error) {
|
2018-12-06 23:50:17 +02:00
|
|
|
domain = dns01.UnFqdn(domain)
|
2018-10-09 21:58:32 +02:00
|
|
|
tld, err := publicsuffix.EffectiveTLDPlusOne(domain)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := d.newRequest(http.MethodGet, "/zones", nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
query := req.URL.Query()
|
|
|
|
query.Add("page_request.filter", fmt.Sprintf("domain='%s'", tld))
|
|
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
|
|
|
|
var zones Zones
|
|
|
|
err = d.do(req, &zones)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(zones.Zones) == 0 {
|
|
|
|
return nil, fmt.Errorf("did not find zone with domain %s", domain)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &zones.Zones[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.stackpath.com/en/api/dns/#operation/GetZoneRecords
|
|
|
|
func (d *DNSProvider) getZoneRecords(name string, zone *Zone) ([]Record, error) {
|
|
|
|
u := fmt.Sprintf("/zones/%s/records", zone.ID)
|
|
|
|
req, err := d.newRequest(http.MethodGet, u, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
query := req.URL.Query()
|
|
|
|
query.Add("page_request.filter", fmt.Sprintf("name='%s' and type='TXT'", name))
|
|
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
|
|
|
|
var records Records
|
|
|
|
err = d.do(req, &records)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(records.Records) == 0 {
|
|
|
|
return nil, fmt.Errorf("did not find record with name %s", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
return records.Records, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.stackpath.com/en/api/dns/#operation/CreateZoneRecord
|
|
|
|
func (d *DNSProvider) createZoneRecord(zone *Zone, record Record) error {
|
|
|
|
u := fmt.Sprintf("/zones/%s/records", zone.ID)
|
|
|
|
req, err := d.newRequest(http.MethodPost, u, record)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.do(req, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://developer.stackpath.com/en/api/dns/#operation/DeleteZoneRecord
|
|
|
|
func (d *DNSProvider) deleteZoneRecord(zone *Zone, record Record) error {
|
|
|
|
u := fmt.Sprintf("/zones/%s/records/%s", zone.ID, record.ID)
|
|
|
|
req, err := d.newRequest(http.MethodDelete, u, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.do(req, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DNSProvider) newRequest(method, urlStr string, body interface{}) (*http.Request, error) {
|
|
|
|
u, err := d.BaseURL.Parse(path.Join(d.config.StackID, urlStr))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if body == nil {
|
|
|
|
var req *http.Request
|
|
|
|
req, err = http.NewRequest(method, u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
reqBody, err := json.Marshal(body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(reqBody))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DNSProvider) do(req *http.Request, v interface{}) error {
|
|
|
|
resp, err := d.client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = checkResponse(resp)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if v == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
raw, err := readBody(resp)
|
|
|
|
if err != nil {
|
2020-02-27 20:14:46 +02:00
|
|
|
return fmt.Errorf("failed to read body: %w", err)
|
2018-10-09 21:58:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
err = json.Unmarshal(raw, v)
|
|
|
|
if err != nil {
|
2020-02-27 20:14:46 +02:00
|
|
|
return fmt.Errorf("unmarshaling error: %w: %s", err, string(raw))
|
2018-10-09 21:58:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkResponse(resp *http.Response) error {
|
|
|
|
if resp.StatusCode > 299 {
|
|
|
|
data, err := readBody(resp)
|
|
|
|
if err != nil {
|
|
|
|
return &ErrorResponse{Code: resp.StatusCode, Message: err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
errResp := &ErrorResponse{}
|
|
|
|
err = json.Unmarshal(data, errResp)
|
|
|
|
if err != nil {
|
|
|
|
return &ErrorResponse{Code: resp.StatusCode, Message: fmt.Sprintf("unmarshaling error: %v: %s", err, string(data))}
|
|
|
|
}
|
|
|
|
return errResp
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func readBody(resp *http.Response) ([]byte, error) {
|
|
|
|
if resp.Body == nil {
|
2020-02-27 20:14:46 +02:00
|
|
|
return nil, errors.New("response body is nil")
|
2018-10-09 21:58:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
rawBody, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return rawBody, nil
|
|
|
|
}
|