diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6bc07e7..204d2e04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,12 +3,12 @@
## Unreleased
### Added:
-- CLI: The `--dns` switch. To include the DNS challenge for consideration. Supported are the following solvers: cloudflare, digitalocean, dnsimple, route53, rfc2136 and manual.
+- CLI: The `--dns` switch. To include the DNS challenge for consideration. Supported are the following solvers: cloudflare, digitalocean, dnsimple, gandi, route53, rfc2136 and manual.
- CLI: The `--accept-tos` switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you.
- lib: A new type for challenge identifiers `Challenge`
- lib: A new interface for custom challenge providers `ChallengeProvider`
- lib: SetChallengeProvider function. Pass a challenge identifier and a Provider to replace the default behaviour of a challenge.
-- lib: The DNS-01 challenge has been implemented with modular solvers using the `ChallengeProvider` interface. Included solvers are: cloudflare, digitalocean, dnsimple, route53, rfc2136 and manual.
+- lib: The DNS-01 challenge has been implemented with modular solvers using the `ChallengeProvider` interface. Included solvers are: cloudflare, digitalocean, dnsimple, gandi, route53, rfc2136 and manual.
### Changed
- lib: ExcludeChallenges now expects to be passed an array of `Challenge` types.
diff --git a/README.md b/README.md
index 7208a1c2..500f8fed 100644
--- a/README.md
+++ b/README.md
@@ -98,6 +98,7 @@ GLOBAL OPTIONS:
cloudflare: CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY
digitalocean: DO_AUTH_TOKEN
dnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY
+ gandi: GANDI_API_KEY
route53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER
manual: none
diff --git a/cli.go b/cli.go
index 83360a45..b61c0508 100644
--- a/cli.go
+++ b/cli.go
@@ -137,6 +137,7 @@ func main() {
"\n\tcloudflare: CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY" +
"\n\tdigitalocean: DO_AUTH_TOKEN" +
"\n\tdnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY" +
+ "\n\tgandi: GANDI_API_KEY" +
"\n\troute53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION" +
"\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER" +
"\n\tmanual: none",
diff --git a/cli_handlers.go b/cli_handlers.go
index 4faa1a62..2e025c56 100644
--- a/cli_handlers.go
+++ b/cli_handlers.go
@@ -14,6 +14,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/gandi"
"github.com/xenolf/lego/providers/dns/rfc2136"
"github.com/xenolf/lego/providers/dns/route53"
"github.com/xenolf/lego/providers/http/webroot"
@@ -92,6 +93,9 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
provider, err = digitalocean.NewDNSProvider(authToken)
case "dnsimple":
provider, err = dnsimple.NewDNSProvider("", "")
+ case "gandi":
+ apiKey := os.Getenv("GANDI_API_KEY")
+ provider, err = gandi.NewDNSProvider(apiKey)
case "route53":
awsRegion := os.Getenv("AWS_REGION")
provider, err = route53.NewDNSProvider("", "", awsRegion)
diff --git a/providers/dns/gandi/gandi.go b/providers/dns/gandi/gandi.go
new file mode 100644
index 00000000..fa1f88e6
--- /dev/null
+++ b/providers/dns/gandi/gandi.go
@@ -0,0 +1,476 @@
+// Package gandi implements a DNS provider for solving the DNS-01
+// challenge using Gandi DNS.
+package gandi
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/xenolf/lego/acme"
+)
+
+// Gandi API reference: http://doc.rpc.gandi.net/index.html
+// Gandi API domain examples: http://doc.rpc.gandi.net/domain/faq.html
+
+type inProgressInfo struct {
+ zoneID int // zoneID of zone to restore in CleanUp
+ newZoneID int // zoneID of temporary zone containing TXT record
+ rootDN string // the registered (root) domain name being manipulated
+}
+
+// DNSProvider is an implementation of the
+// acme.ChallengeProviderTimeout interface that uses Gandi's XML-RPC
+// API to manage TXT records for a domain.
+type DNSProvider struct {
+ apiKey string
+ inProgressFQDNs map[string]inProgressInfo
+ inProgressRootDNs map[string]struct{}
+ inProgressMu sync.Mutex
+}
+
+// NewDNSProvider returns a new DNSProvider instance. apiKey is the
+// API access key obtained from the Gandi account control panel.
+func NewDNSProvider(apiKey string) (*DNSProvider, error) {
+ if apiKey == "" {
+ return nil, fmt.Errorf("No Gandi API Key given")
+ }
+ return &DNSProvider{
+ apiKey: apiKey,
+ inProgressFQDNs: make(map[string]inProgressInfo),
+ inProgressRootDNs: make(map[string]struct{}),
+ }, nil
+}
+
+// Present creates a TXT record using the specified parameters. It
+// does this by creating and activating a new temporary 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
+ }
+ i := strings.Index(fqdn, ".")
+ sub := fqdn[:i+1]
+ root := fqdn[i+1:]
+ var zoneID int
+ var err error
+ // find sub and root (sub + root == fqdn) where root is the domain
+ // registered with gandi. Do this by successively increasing sub
+ // and decreasing root until root matches a registered domain with
+ // a zone_id
+ for {
+ zoneID, err = d.getZoneID(root)
+ if err == nil {
+ // domain found
+ break
+ }
+ if faultErr, ok := err.(rpcError); ok {
+ if faultErr.faultCode == 510042 {
+ // 510042 error means root is not found - increase
+ // sub, reduce root and retry.
+ // [see http://doc.rpc.gandi.net/errors/fault_codes.html]
+ i := strings.Index(root, ".")
+ if i != -1 && i != len(root)-1 &&
+ strings.Index(root[i+1:], ".") != -1 &&
+ strings.Index(root[i+1:], ".") != len(root[i+1:])-1 {
+ sub = sub + root[:i+1]
+ root = root[i+1:]
+ continue
+ }
+ }
+ }
+ // root is not found and cannot be reduced in size any further
+ // or there is some other error from getZoneID
+ return err
+ }
+ // remove trailing "." from sub
+ sub = sub[:len(sub)-1]
+ // acquire lock and check there is not a challenge already in
+ // progress for this value of root
+ d.inProgressMu.Lock()
+ defer d.inProgressMu.Unlock()
+ if _, ok := d.inProgressRootDNs[root]; ok {
+ return fmt.Errorf(
+ "Gandi DNS: challenge already in progress on root domain")
+ }
+ // perform API actions to create and activate new zone for root
+ // containing the required TXT record
+ newZoneName := fmt.Sprintf(
+ "%s [ACME Challenge %s]",
+ root[:len(root)-1], 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, sub, value, ttl)
+ if err != nil {
+ return err
+ }
+ err = d.setZoneVersion(newZoneID, newZoneVersion)
+ if err != nil {
+ return err
+ }
+ err = d.setZone(root, newZoneID)
+ if err != nil {
+ return err
+ }
+ // save data necessary for CleanUp
+ d.inProgressFQDNs[fqdn] = inProgressInfo{
+ zoneID: zoneID,
+ newZoneID: newZoneID,
+ rootDN: root,
+ }
+ d.inProgressRootDNs[root] = struct{}{}
+ return nil
+}
+
+// CleanUp removes the TXT record matching the specified
+// parameters. It does this by restoring the old DNS zone and removing
+// the temporary one created by Present.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
+ // acquire lock and retrieve zoneID, newZoneID and root
+ d.inProgressMu.Lock()
+ defer d.inProgressMu.Unlock()
+ if _, ok := d.inProgressFQDNs[fqdn]; !ok {
+ // if there is no cleanup information then just return
+ return nil
+ }
+ zoneID := d.inProgressFQDNs[fqdn].zoneID
+ newZoneID := d.inProgressFQDNs[fqdn].newZoneID
+ root := d.inProgressFQDNs[fqdn].rootDN
+ delete(d.inProgressFQDNs, fqdn)
+ delete(d.inProgressRootDNs, root)
+ // perform API actions to restore old zone for root
+ err := d.setZone(root, zoneID)
+ if err != nil {
+ return err
+ }
+ err = d.deleteZone(newZoneID)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// Timeout returns the values (40*time.Minute, 60*time.Second) which
+// are used by the acme package as timeout and check interval values
+// when checking for DNS record propagation with Gandi.
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return 40 * time.Minute, 60 * time.Second
+}
+
+// Endpoint is the Gandi XML-RPC endpoint used by Present and
+// CleanUp. It is exported only so it may be overridden during package
+// tests.
+var Endpoint = "https://rpc.gandi.net/xmlrpc/"
+
+// types for XML-RPC method calls and parameters
+
+type param interface {
+ param()
+}
+type paramString struct {
+ XMLName xml.Name `xml:"param"`
+ Value string `xml:"value>string"`
+}
+type paramInt struct {
+ XMLName xml.Name `xml:"param"`
+ Value int `xml:"value>int"`
+}
+
+type structMember interface {
+ structMember()
+}
+type structMemberString struct {
+ Name string `xml:"name"`
+ Value string `xml:"value>string"`
+}
+type structMemberInt struct {
+ Name string `xml:"name"`
+ Value int `xml:"value>int"`
+}
+type paramStruct struct {
+ XMLName xml.Name `xml:"param"`
+ StructMembers []structMember `xml:"value>struct>member"`
+}
+
+func (p paramString) param() {}
+func (p paramInt) param() {}
+func (m structMemberString) structMember() {}
+func (m structMemberInt) structMember() {}
+func (p paramStruct) param() {}
+
+type methodCall struct {
+ XMLName xml.Name `xml:"methodCall"`
+ MethodName string `xml:"methodName"`
+ Params []param `xml:"params"`
+}
+
+// types for XML-RPC responses
+
+type response interface {
+ faultCode() int
+ faultString() string
+}
+
+type responseFault struct {
+ FaultCode int `xml:"fault>value>struct>member>value>int"`
+ FaultString string `xml:"fault>value>struct>member>value>string"`
+}
+
+func (r responseFault) faultCode() int { return r.FaultCode }
+func (r responseFault) faultString() string { return r.FaultString }
+
+type responseStruct struct {
+ responseFault
+ StructMembers []struct {
+ Name string `xml:"name"`
+ ValueInt int `xml:"value>int"`
+ } `xml:"params>param>value>struct>member"`
+}
+
+type responseInt struct {
+ responseFault
+ Value int `xml:"params>param>value>int"`
+}
+
+type responseBool struct {
+ responseFault
+ Value bool `xml:"params>param>value>boolean"`
+}
+
+// POSTing/Marshalling/Unmarshalling
+
+type rpcError struct {
+ faultCode int
+ faultString string
+}
+
+func (e rpcError) Error() string {
+ return fmt.Sprintf(
+ "Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString)
+}
+
+func httpPost(url string, bodyType string, body io.Reader) ([]byte, error) {
+ client := http.Client{Timeout: 60 * time.Second}
+ resp, err := client.Post(url, bodyType, body)
+ if err != nil {
+ return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
+ }
+ defer resp.Body.Close()
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Gandi DNS: HTTP Post Error: %v", err)
+ }
+ return b, nil
+}
+
+// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by
+// marshalling the data given in the call argument to XML and sending
+// that via HTTP Post to Gandi. The response is then unmarshalled into
+// the resp argument.
+func rpcCall(call *methodCall, resp response) error {
+ // marshal
+ b, err := xml.MarshalIndent(call, "", " ")
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: Marshal Error: %v", err)
+ }
+ // post
+ b = append([]byte(``+"\n"), b...)
+ respBody, err := httpPost(Endpoint, "text/xml", bytes.NewReader(b))
+ if err != nil {
+ return err
+ }
+ // unmarshal
+ err = xml.Unmarshal(respBody, resp)
+ if err != nil {
+ return fmt.Errorf("Gandi DNS: Unmarshal Error: %v", err)
+ }
+ if resp.faultCode() != 0 {
+ return rpcError{
+ faultCode: resp.faultCode(), faultString: resp.faultString()}
+ }
+ return nil
+}
+
+// functions to perform API actions
+
+func (d *DNSProvider) getZoneID(domain string) (int, error) {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.info",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramString{Value: domain},
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ var zoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "zone_id" {
+ zoneID = member.ValueInt
+ }
+ }
+ if zoneID == 0 {
+ return 0, fmt.Errorf("Gandi DNS: Could not determine zone_id")
+ }
+ return zoneID, nil
+}
+
+func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.clone",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: 0},
+ paramStruct{
+ StructMembers: []structMember{
+ structMemberString{
+ Name: "name",
+ Value: name,
+ }},
+ },
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ var newZoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "id" {
+ newZoneID = member.ValueInt
+ }
+ }
+ if newZoneID == 0 {
+ return 0, fmt.Errorf("Gandi DNS: Could not determine cloned zone_id")
+ }
+ return newZoneID, nil
+}
+
+func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) {
+ resp := &responseInt{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.version.new",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return 0, err
+ }
+ if resp.Value == 0 {
+ return 0, fmt.Errorf("Gandi DNS: Could not create new zone version")
+ }
+ return resp.Value, nil
+}
+
+func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value string, ttl int) error {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.record.add",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: version},
+ paramStruct{
+ StructMembers: []structMember{
+ structMemberString{
+ Name: "type",
+ Value: "TXT",
+ }, structMemberString{
+ Name: "name",
+ Value: name,
+ }, structMemberString{
+ Name: "value",
+ Value: value,
+ }, structMemberInt{
+ Name: "ttl",
+ Value: ttl,
+ }},
+ },
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (d *DNSProvider) setZoneVersion(zoneID int, version int) error {
+ resp := &responseBool{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.version.set",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ paramInt{Value: version},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ if !resp.Value {
+ return fmt.Errorf("Gandi DNS: could not set zone version")
+ }
+ return nil
+}
+
+func (d *DNSProvider) setZone(domain string, zoneID int) error {
+ resp := &responseStruct{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.set",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramString{Value: domain},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ var respZoneID int
+ for _, member := range resp.StructMembers {
+ if member.Name == "zone_id" {
+ respZoneID = member.ValueInt
+ }
+ }
+ if respZoneID != zoneID {
+ return fmt.Errorf("Gandi DNS: Could not set new zone_id")
+ }
+ return nil
+}
+
+func (d *DNSProvider) deleteZone(zoneID int) error {
+ resp := &responseBool{}
+ err := rpcCall(&methodCall{
+ MethodName: "domain.zone.delete",
+ Params: []param{
+ paramString{Value: d.apiKey},
+ paramInt{Value: zoneID},
+ },
+ }, resp)
+ if err != nil {
+ return err
+ }
+ if !resp.Value {
+ return fmt.Errorf("Gandi DNS: could not delete zone_id")
+ }
+ return nil
+}
diff --git a/providers/dns/gandi/gandi_test.go b/providers/dns/gandi/gandi_test.go
new file mode 100644
index 00000000..1f44a962
--- /dev/null
+++ b/providers/dns/gandi/gandi_test.go
@@ -0,0 +1,916 @@
+package gandi_test
+
+import (
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/xenolf/lego/providers/dns/gandi"
+)
+
+// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
+// Server, whose responses are predetermined for particular requests.
+func TestDNSProvider(t *testing.T) {
+ fakeAPIKey := "123412341234123412341234"
+ fakeKeyAuth := "XXXX"
+ provider, err := gandi.NewDNSProvider(fakeAPIKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ regexpDate, err := regexp.Compile(`\[ACME Challenge [^\]:]*:[^\]]*\]`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // start fake RPC server
+ fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Header.Get("Content-Type") != "text/xml" {
+ t.Fatalf("Content-Type: text/xml header not found")
+ }
+ req, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req = regexpDate.ReplaceAllLiteral(
+ req, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`))
+ resp, ok := serverResponses[string(req)]
+ if !ok {
+ t.Fatalf("Server response for request not found")
+ }
+ _, err = io.Copy(w, strings.NewReader(resp))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }))
+ defer fakeServer.Close()
+ // override gandi endpoint to point to fake server
+ savedEndpoint := gandi.Endpoint
+ defer func() {
+ gandi.Endpoint = savedEndpoint
+ }()
+ gandi.Endpoint = fakeServer.URL + "/"
+ // run Present
+ err = provider.Present("abc.def.example.com", "", fakeKeyAuth)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // run CleanUp
+ err = provider.CleanUp("abc.def.example.com", "", fakeKeyAuth)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// serverResponses is the XML-RPC Request->Response map used by the
+// fake RPC server. It was generated by recording a real RPC session
+// which resulted in the successful issue of a cert, and then
+// anonymizing the RPC data.
+var serverResponses = map[string]string{
+ // Present Request->Response 1 (getZoneID)
+ `
+
+ domain.info
+
+
+ 123412341234123412341234
+
+
+
+
+ abc.def.example.com.
+
+
+`: `
+
+
+
+
+faultCode
+510042
+
+
+faultString
+Error on object : OBJECT_DOMAIN (CAUSE_NOTFOUND) [Domain 'abc.def.example.com.' doesn't exist.]
+
+
+
+
+`,
+ // Present Request->Response 2 (getZoneID)
+ `
+
+ domain.info
+
+
+ 123412341234123412341234
+
+
+
+
+ def.example.com.
+
+
+`: `
+
+
+
+
+faultCode
+510042
+
+
+faultString
+Error on object : OBJECT_DOMAIN (CAUSE_NOTFOUND) [Domain 'def.example.com.' doesn't exist.]
+
+
+
+
+`,
+ // Present Request->Response 3 (getZoneID)
+ `
+
+ domain.info
+
+
+ 123412341234123412341234
+
+
+
+
+ example.com.
+
+
+`: `
+
+
+
+
+
+date_updated
+20160216T16:14:23
+
+
+date_delete
+20170331T16:04:06
+
+
+is_premium
+0
+
+
+date_hold_begin
+20170215T02:04:06
+
+
+date_registry_end
+20170215T02:04:06
+
+
+authinfo_expiration_date
+20161211T21:31:20
+
+
+contacts
+
+
+owner
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+admin
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+bill
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+tech
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+reseller
+
+
+
+
+nameservers
+
+a.dns.gandi.net
+b.dns.gandi.net
+c.dns.gandi.net
+
+
+
+date_restore_end
+20170501T02:04:06
+
+
+id
+2222222
+
+
+authinfo
+ABCDABCDAB
+
+
+status
+
+clientTransferProhibited
+serverTransferProhibited
+
+
+
+tags
+
+
+
+
+date_hold_end
+20170401T02:04:06
+
+
+services
+
+gandidns
+gandimail
+
+
+
+date_pending_delete_end
+20170506T02:04:06
+
+
+zone_id
+1234567
+
+
+date_renew_begin
+20120101T00:00:00
+
+
+fqdn
+example.com
+
+
+autorenew
+
+
+date_registry_creation
+20150215T02:04:06
+
+
+tld
+org
+
+
+date_created
+20150215T03:04:06
+
+
+
+
+
+`,
+ // Present Request->Response 4 (cloneZone)
+ `
+
+ domain.zone.clone
+
+
+ 123412341234123412341234
+
+
+
+
+ 1234567
+
+
+
+
+ 0
+
+
+
+
+
+
+ name
+
+ example.com [ACME Challenge 01 Jan 16 00:00 +0000]
+
+
+
+
+
+`: `
+
+
+
+
+
+name
+example.com [ACME Challenge 01 Jan 16 00:00 +0000]
+
+
+versions
+
+1
+
+
+
+date_updated
+20160216T16:24:29
+
+
+id
+7654321
+
+
+owner
+LEGO-GANDI
+
+
+version
+1
+
+
+domains
+0
+
+
+public
+0
+
+
+
+
+
+`,
+ // Present Request->Response 5 (newZoneVersion)
+ `
+
+ domain.zone.version.new
+
+
+ 123412341234123412341234
+
+
+
+
+ 7654321
+
+
+`: `
+
+
+
+2
+
+
+
+`,
+ // Present Request->Response 6 (addTXTRecord)
+ `
+
+ domain.zone.record.add
+
+
+ 123412341234123412341234
+
+
+
+
+ 7654321
+
+
+
+
+ 2
+
+
+
+
+
+
+ type
+
+ TXT
+
+
+
+ name
+
+ _acme-challenge.abc.def
+
+
+
+ value
+
+ ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ
+
+
+
+ ttl
+
+ 300
+
+
+
+
+
+`: `
+
+
+
+
+
+name
+_acme-challenge.abc.def
+
+
+type
+TXT
+
+
+id
+3333333333
+
+
+value
+"ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ"
+
+
+ttl
+300
+
+
+
+
+
+`,
+ // Present Request->Response 7 (setZoneVersion)
+ `
+
+ domain.zone.version.set
+
+
+ 123412341234123412341234
+
+
+
+
+ 7654321
+
+
+
+
+ 2
+
+
+`: `
+
+
+
+1
+
+
+
+`,
+ // Present Request->Response 8 (setZone)
+ `
+
+ domain.zone.set
+
+
+ 123412341234123412341234
+
+
+
+
+ example.com.
+
+
+
+
+ 7654321
+
+
+`: `
+
+
+
+
+
+date_updated
+20160216T16:14:23
+
+
+date_delete
+20170331T16:04:06
+
+
+is_premium
+0
+
+
+date_hold_begin
+20170215T02:04:06
+
+
+date_registry_end
+20170215T02:04:06
+
+
+authinfo_expiration_date
+20161211T21:31:20
+
+
+contacts
+
+
+owner
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+admin
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+bill
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+tech
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+reseller
+
+
+
+
+nameservers
+
+a.dns.gandi.net
+b.dns.gandi.net
+c.dns.gandi.net
+
+
+
+date_restore_end
+20170501T02:04:06
+
+
+id
+2222222
+
+
+authinfo
+ABCDABCDAB
+
+
+status
+
+clientTransferProhibited
+serverTransferProhibited
+
+
+
+tags
+
+
+
+
+date_hold_end
+20170401T02:04:06
+
+
+services
+
+gandidns
+gandimail
+
+
+
+date_pending_delete_end
+20170506T02:04:06
+
+
+zone_id
+7654321
+
+
+date_renew_begin
+20120101T00:00:00
+
+
+fqdn
+example.com
+
+
+autorenew
+
+
+date_registry_creation
+20150215T02:04:06
+
+
+tld
+org
+
+
+date_created
+20150215T03:04:06
+
+
+
+
+
+`,
+ // CleanUp Request->Response 1 (setZone)
+ `
+
+ domain.zone.set
+
+
+ 123412341234123412341234
+
+
+
+
+ example.com.
+
+
+
+
+ 1234567
+
+
+`: `
+
+
+
+
+
+date_updated
+20160216T16:24:38
+
+
+date_delete
+20170331T16:04:06
+
+
+is_premium
+0
+
+
+date_hold_begin
+20170215T02:04:06
+
+
+date_registry_end
+20170215T02:04:06
+
+
+authinfo_expiration_date
+20161211T21:31:20
+
+
+contacts
+
+
+owner
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+admin
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+bill
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+tech
+
+
+handle
+LEGO-GANDI
+
+
+id
+111111
+
+
+
+
+reseller
+
+
+
+
+nameservers
+
+a.dns.gandi.net
+b.dns.gandi.net
+c.dns.gandi.net
+
+
+
+date_restore_end
+20170501T02:04:06
+
+
+id
+2222222
+
+
+authinfo
+ABCDABCDAB
+
+
+status
+
+clientTransferProhibited
+serverTransferProhibited
+
+
+
+tags
+
+
+
+
+date_hold_end
+20170401T02:04:06
+
+
+services
+
+gandidns
+gandimail
+
+
+
+date_pending_delete_end
+20170506T02:04:06
+
+
+zone_id
+1234567
+
+
+date_renew_begin
+20120101T00:00:00
+
+
+fqdn
+example.com
+
+
+autorenew
+
+
+date_registry_creation
+20150215T02:04:06
+
+
+tld
+org
+
+
+date_created
+20150215T03:04:06
+
+
+
+
+
+`,
+ // CleanUp Request->Response 2 (deleteZone)
+ `
+
+ domain.zone.delete
+
+
+ 123412341234123412341234
+
+
+
+
+ 7654321
+
+
+`: `
+
+
+
+1
+
+
+
+`,
+}