From 0568e817012753a9f71fffa50160990dbc3779a2 Mon Sep 17 00:00:00 2001
From: Felix Lange <fjl@twurst.com>
Date: Wed, 25 Sep 2019 11:38:13 +0200
Subject: [PATCH] p2p/dnsdisc: add implementation of EIP-1459 (#20094)

This adds an implementation of node discovery via DNS TXT records to the
go-ethereum library. The implementation doesn't match EIP-1459 exactly,
the main difference being that this implementation uses separate merkle
trees for tree links and ENRs. The EIP will be updated to match p2p/dnsdisc.

To maintain DNS trees, cmd/devp2p provides a frontend for the p2p/dnsdisc
library. The new 'dns' subcommands can be used to create, sign and deploy DNS
discovery trees.
---
 cmd/devp2p/discv4cmd.go                       | 120 +--
 cmd/devp2p/dns_cloudflare.go                  | 163 ++++
 cmd/devp2p/dnscmd.go                          | 358 +++++++++
 cmd/devp2p/main.go                            |  34 +-
 cmd/devp2p/nodeset.go                         |  87 ++
 p2p/dnsdisc/client.go                         | 260 ++++++
 p2p/dnsdisc/client_test.go                    | 306 ++++++++
 p2p/dnsdisc/doc.go                            |  18 +
 p2p/dnsdisc/error.go                          |  63 ++
 p2p/dnsdisc/sync.go                           | 277 +++++++
 p2p/dnsdisc/tree.go                           | 384 +++++++++
 p2p/dnsdisc/tree_test.go                      | 144 ++++
 .../cloudflare-go/CODE_OF_CONDUCT.md          |  77 ++
 .../cloudflare/cloudflare-go/LICENSE          |  26 +
 .../cloudflare/cloudflare-go/README.md        | 107 +++
 .../cloudflare-go/access_application.go       | 180 +++++
 .../cloudflare-go/access_identity_provider.go | 331 ++++++++
 .../cloudflare-go/access_organization.go      | 101 +++
 .../cloudflare/cloudflare-go/access_policy.go | 221 ++++++
 .../cloudflare-go/access_service_tokens.go    | 167 ++++
 .../cloudflare-go/account_members.go          | 186 +++++
 .../cloudflare/cloudflare-go/account_roles.go |  80 ++
 .../cloudflare/cloudflare-go/accounts.go      | 114 +++
 .../cloudflare/cloudflare-go/argo.go          | 120 +++
 .../cloudflare/cloudflare-go/auditlogs.go     | 143 ++++
 .../cloudflare/cloudflare-go/cloudflare.go    | 435 ++++++++++
 .../cloudflare-go/custom_hostname.go          | 161 ++++
 .../cloudflare/cloudflare-go/custom_pages.go  | 176 +++++
 .../cloudflare/cloudflare-go/dns.go           | 174 ++++
 .../cloudflare/cloudflare-go/duration.go      |  40 +
 .../cloudflare/cloudflare-go/errors.go        |  50 ++
 .../cloudflare/cloudflare-go/filter.go        | 241 ++++++
 .../cloudflare/cloudflare-go/firewall.go      | 280 +++++++
 .../cloudflare-go/firewall_rules.go           | 196 +++++
 .../cloudflare/cloudflare-go/go.mod           |  13 +
 .../cloudflare/cloudflare-go/go.sum           |  26 +
 .../cloudflare/cloudflare-go/ips.go           |  44 ++
 .../cloudflare/cloudflare-go/keyless.go       |  52 ++
 .../cloudflare-go/load_balancing.go           | 387 +++++++++
 .../cloudflare/cloudflare-go/lockdown.go      | 151 ++++
 .../cloudflare/cloudflare-go/logpush.go       | 224 ++++++
 .../cloudflare/cloudflare-go/options.go       | 101 +++
 .../cloudflare/cloudflare-go/origin_ca.go     | 169 ++++
 .../cloudflare/cloudflare-go/page_rules.go    | 235 ++++++
 .../cloudflare/cloudflare-go/railgun.go       | 297 +++++++
 .../cloudflare/cloudflare-go/rate_limiting.go | 210 +++++
 .../cloudflare/cloudflare-go/registrar.go     | 175 +++++
 .../cloudflare/cloudflare-go/renovate.json    |   5 +
 .../cloudflare/cloudflare-go/spectrum.go      | 158 ++++
 .../cloudflare/cloudflare-go/ssl.go           | 157 ++++
 .../cloudflare/cloudflare-go/universal_ssl.go |  88 +++
 .../cloudflare/cloudflare-go/user.go          | 113 +++
 .../cloudflare/cloudflare-go/user_agent.go    | 149 ++++
 .../cloudflare/cloudflare-go/virtualdns.go    | 192 +++++
 .../cloudflare/cloudflare-go/waf.go           | 300 +++++++
 .../cloudflare/cloudflare-go/workers.go       | 314 ++++++++
 .../cloudflare/cloudflare-go/workers_kv.go    | 192 +++++
 .../cloudflare/cloudflare-go/zone.go          | 740 ++++++++++++++++++
 vendor/golang.org/x/time/LICENSE              |  27 +
 vendor/golang.org/x/time/PATENTS              |  22 +
 vendor/golang.org/x/time/rate/rate.go         | 374 +++++++++
 vendor/vendor.json                            |  12 +
 62 files changed, 10698 insertions(+), 49 deletions(-)
 create mode 100644 cmd/devp2p/dns_cloudflare.go
 create mode 100644 cmd/devp2p/dnscmd.go
 create mode 100644 cmd/devp2p/nodeset.go
 create mode 100644 p2p/dnsdisc/client.go
 create mode 100644 p2p/dnsdisc/client_test.go
 create mode 100644 p2p/dnsdisc/doc.go
 create mode 100644 p2p/dnsdisc/error.go
 create mode 100644 p2p/dnsdisc/sync.go
 create mode 100644 p2p/dnsdisc/tree.go
 create mode 100644 p2p/dnsdisc/tree_test.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/CODE_OF_CONDUCT.md
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/LICENSE
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/README.md
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/access_application.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/access_identity_provider.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/access_organization.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/access_policy.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/access_service_tokens.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/account_members.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/account_roles.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/accounts.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/argo.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/auditlogs.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cloudflare.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/custom_hostname.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/custom_pages.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/dns.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/duration.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/errors.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/filter.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/firewall.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/firewall_rules.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/go.mod
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/go.sum
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/ips.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/keyless.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/load_balancing.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/lockdown.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/logpush.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/options.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/origin_ca.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/page_rules.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/railgun.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/rate_limiting.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/registrar.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/renovate.json
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/spectrum.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/ssl.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/universal_ssl.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/user.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/user_agent.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/virtualdns.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/waf.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/workers.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/workers_kv.go
 create mode 100644 vendor/github.com/cloudflare/cloudflare-go/zone.go
 create mode 100644 vendor/golang.org/x/time/LICENSE
 create mode 100644 vendor/golang.org/x/time/PATENTS
 create mode 100644 vendor/golang.org/x/time/rate/rate.go

diff --git a/cmd/devp2p/discv4cmd.go b/cmd/devp2p/discv4cmd.go
index 1e56687a6..ab5b87402 100644
--- a/cmd/devp2p/discv4cmd.go
+++ b/cmd/devp2p/discv4cmd.go
@@ -19,10 +19,10 @@ package main
 import (
 	"fmt"
 	"net"
-	"sort"
 	"strings"
 	"time"
 
+	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/crypto"
 	"github.com/ethereum/go-ethereum/p2p/discover"
 	"github.com/ethereum/go-ethereum/p2p/enode"
@@ -38,23 +38,34 @@ var (
 			discv4PingCommand,
 			discv4RequestRecordCommand,
 			discv4ResolveCommand,
+			discv4ResolveJSONCommand,
 		},
 	}
 	discv4PingCommand = cli.Command{
-		Name:   "ping",
-		Usage:  "Sends ping to a node",
-		Action: discv4Ping,
+		Name:      "ping",
+		Usage:     "Sends ping to a node",
+		Action:    discv4Ping,
+		ArgsUsage: "<node>",
 	}
 	discv4RequestRecordCommand = cli.Command{
-		Name:   "requestenr",
-		Usage:  "Requests a node record using EIP-868 enrRequest",
-		Action: discv4RequestRecord,
+		Name:      "requestenr",
+		Usage:     "Requests a node record using EIP-868 enrRequest",
+		Action:    discv4RequestRecord,
+		ArgsUsage: "<node>",
 	}
 	discv4ResolveCommand = cli.Command{
-		Name:   "resolve",
-		Usage:  "Finds a node in the DHT",
-		Action: discv4Resolve,
-		Flags:  []cli.Flag{bootnodesFlag},
+		Name:      "resolve",
+		Usage:     "Finds a node in the DHT",
+		Action:    discv4Resolve,
+		ArgsUsage: "<node>",
+		Flags:     []cli.Flag{bootnodesFlag},
+	}
+	discv4ResolveJSONCommand = cli.Command{
+		Name:      "resolve-json",
+		Usage:     "Re-resolves nodes in a nodes.json file",
+		Action:    discv4ResolveJSON,
+		Flags:     []cli.Flag{bootnodesFlag},
+		ArgsUsage: "<nodes.json file>",
 	}
 )
 
@@ -64,10 +75,8 @@ var bootnodesFlag = cli.StringFlag{
 }
 
 func discv4Ping(ctx *cli.Context) error {
-	n, disc, err := getNodeArgAndStartV4(ctx)
-	if err != nil {
-		return err
-	}
+	n := getNodeArg(ctx)
+	disc := startV4(ctx)
 	defer disc.Close()
 
 	start := time.Now()
@@ -79,10 +88,8 @@ func discv4Ping(ctx *cli.Context) error {
 }
 
 func discv4RequestRecord(ctx *cli.Context) error {
-	n, disc, err := getNodeArgAndStartV4(ctx)
-	if err != nil {
-		return err
-	}
+	n := getNodeArg(ctx)
+	disc := startV4(ctx)
 	defer disc.Close()
 
 	respN, err := disc.RequestENR(n)
@@ -94,33 +101,43 @@ func discv4RequestRecord(ctx *cli.Context) error {
 }
 
 func discv4Resolve(ctx *cli.Context) error {
-	n, disc, err := getNodeArgAndStartV4(ctx)
-	if err != nil {
-		return err
-	}
+	n := getNodeArg(ctx)
+	disc := startV4(ctx)
 	defer disc.Close()
 
 	fmt.Println(disc.Resolve(n).String())
 	return nil
 }
 
-func getNodeArgAndStartV4(ctx *cli.Context) (*enode.Node, *discover.UDPv4, error) {
-	if ctx.NArg() != 1 {
-		return nil, nil, fmt.Errorf("missing node as command-line argument")
+func discv4ResolveJSON(ctx *cli.Context) error {
+	if ctx.NArg() < 1 {
+		return fmt.Errorf("need nodes file as argument")
 	}
-	n, err := parseNode(ctx.Args()[0])
-	if err != nil {
-		return nil, nil, err
+	disc := startV4(ctx)
+	defer disc.Close()
+	file := ctx.Args().Get(0)
+
+	// Load existing nodes in file.
+	var nodes []*enode.Node
+	if common.FileExist(file) {
+		nodes = loadNodesJSON(file).nodes()
 	}
-	var bootnodes []*enode.Node
-	if commandHasFlag(ctx, bootnodesFlag) {
-		bootnodes, err = parseBootnodes(ctx)
+	// Add nodes from command line arguments.
+	for i := 1; i < ctx.NArg(); i++ {
+		n, err := parseNode(ctx.Args().Get(i))
 		if err != nil {
-			return nil, nil, err
+			exit(err)
 		}
+		nodes = append(nodes, n)
+	}
+
+	result := make(nodeSet, len(nodes))
+	for _, n := range nodes {
+		n = disc.Resolve(n)
+		result[n.ID()] = nodeJSON{Seq: n.Seq(), N: n}
 	}
-	disc, err := startV4(bootnodes)
-	return n, disc, err
+	writeNodesJSON(file, result)
+	return nil
 }
 
 func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
@@ -139,28 +156,39 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
 	return nodes, nil
 }
 
-// commandHasFlag returns true if the current command supports the given flag.
-func commandHasFlag(ctx *cli.Context, flag cli.Flag) bool {
-	flags := ctx.FlagNames()
-	sort.Strings(flags)
-	i := sort.SearchStrings(flags, flag.GetName())
-	return i != len(flags) && flags[i] == flag.GetName()
+// startV4 starts an ephemeral discovery V4 node.
+func startV4(ctx *cli.Context) *discover.UDPv4 {
+	socket, ln, cfg, err := listen()
+	if err != nil {
+		exit(err)
+	}
+	if commandHasFlag(ctx, bootnodesFlag) {
+		bn, err := parseBootnodes(ctx)
+		if err != nil {
+			exit(err)
+		}
+		cfg.Bootnodes = bn
+	}
+	disc, err := discover.ListenV4(socket, ln, cfg)
+	if err != nil {
+		exit(err)
+	}
+	return disc
 }
 
-// startV4 starts an ephemeral discovery V4 node.
-func startV4(bootnodes []*enode.Node) (*discover.UDPv4, error) {
+func listen() (*net.UDPConn, *enode.LocalNode, discover.Config, error) {
 	var cfg discover.Config
-	cfg.Bootnodes = bootnodes
 	cfg.PrivateKey, _ = crypto.GenerateKey()
 	db, _ := enode.OpenDB("")
 	ln := enode.NewLocalNode(db, cfg.PrivateKey)
 
 	socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{0, 0, 0, 0}})
 	if err != nil {
-		return nil, err
+		db.Close()
+		return nil, nil, cfg, err
 	}
 	addr := socket.LocalAddr().(*net.UDPAddr)
 	ln.SetFallbackIP(net.IP{127, 0, 0, 1})
 	ln.SetFallbackUDP(addr.Port)
-	return discover.ListenUDP(socket, ln, cfg)
+	return socket, ln, cfg, nil
 }
diff --git a/cmd/devp2p/dns_cloudflare.go b/cmd/devp2p/dns_cloudflare.go
new file mode 100644
index 000000000..83279168c
--- /dev/null
+++ b/cmd/devp2p/dns_cloudflare.go
@@ -0,0 +1,163 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/cloudflare/cloudflare-go"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p/dnsdisc"
+	"gopkg.in/urfave/cli.v1"
+)
+
+var (
+	cloudflareTokenFlag = cli.StringFlag{
+		Name:   "token",
+		Usage:  "CloudFlare API token",
+		EnvVar: "CLOUDFLARE_API_TOKEN",
+	}
+	cloudflareZoneIDFlag = cli.StringFlag{
+		Name:  "zoneid",
+		Usage: "CloudFlare Zone ID (optional)",
+	}
+)
+
+type cloudflareClient struct {
+	*cloudflare.API
+	zoneID string
+}
+
+// newCloudflareClient sets up a CloudFlare API client from command line flags.
+func newCloudflareClient(ctx *cli.Context) *cloudflareClient {
+	token := ctx.String(cloudflareTokenFlag.Name)
+	if token == "" {
+		exit(fmt.Errorf("need cloudflare API token to proceed"))
+	}
+	api, err := cloudflare.NewWithAPIToken(token)
+	if err != nil {
+		exit(fmt.Errorf("can't create Cloudflare client: %v", err))
+	}
+	return &cloudflareClient{
+		API:    api,
+		zoneID: ctx.String(cloudflareZoneIDFlag.Name),
+	}
+}
+
+// deploy uploads the given tree to CloudFlare DNS.
+func (c *cloudflareClient) deploy(name string, t *dnsdisc.Tree) error {
+	if err := c.checkZone(name); err != nil {
+		return err
+	}
+	records := t.ToTXT(name)
+	return c.uploadRecords(name, records)
+}
+
+// checkZone verifies permissions on the CloudFlare DNS Zone for name.
+func (c *cloudflareClient) checkZone(name string) error {
+	if c.zoneID == "" {
+		log.Info(fmt.Sprintf("Finding CloudFlare zone ID for %s", name))
+		id, err := c.ZoneIDByName(name)
+		if err != nil {
+			return err
+		}
+		c.zoneID = id
+	}
+	log.Info(fmt.Sprintf("Checking Permissions on zone %s", c.zoneID))
+	zone, err := c.ZoneDetails(c.zoneID)
+	if err != nil {
+		return err
+	}
+	if !strings.HasSuffix(name, "."+zone.Name) {
+		return fmt.Errorf("CloudFlare zone name %q does not match name %q to be deployed", zone.Name, name)
+	}
+	needPerms := map[string]bool{"#zone:edit": false, "#zone:read": false}
+	for _, perm := range zone.Permissions {
+		if _, ok := needPerms[perm]; ok {
+			needPerms[perm] = true
+		}
+	}
+	for _, ok := range needPerms {
+		if !ok {
+			return fmt.Errorf("wrong permissions on zone %s: %v", c.zoneID, needPerms)
+		}
+	}
+	return nil
+}
+
+// uploadRecords updates the TXT records at a particular subdomain. All non-root records
+// will have a TTL of "infinity" and all existing records not in the new map will be
+// nuked!
+func (c *cloudflareClient) uploadRecords(name string, records map[string]string) error {
+	// Convert all names to lowercase.
+	lrecords := make(map[string]string, len(records))
+	for name, r := range records {
+		lrecords[strings.ToLower(name)] = r
+	}
+	records = lrecords
+
+	log.Info(fmt.Sprintf("Retrieving existing TXT records on %s", name))
+	entries, err := c.DNSRecords(c.zoneID, cloudflare.DNSRecord{Type: "TXT"})
+	if err != nil {
+		return err
+	}
+	existing := make(map[string]cloudflare.DNSRecord)
+	for _, entry := range entries {
+		if !strings.HasSuffix(entry.Name, name) {
+			continue
+		}
+		existing[strings.ToLower(entry.Name)] = entry
+	}
+
+	// Iterate over the new records and inject anything missing.
+	for path, val := range records {
+		old, exists := existing[path]
+		if !exists {
+			// Entry is unknown, push a new one to Cloudflare.
+			log.Info(fmt.Sprintf("Creating %s = %q", path, val))
+			ttl := 1
+			if path != name {
+				ttl = 2147483647 // Max TTL permitted by Cloudflare
+			}
+			_, err = c.CreateDNSRecord(c.zoneID, cloudflare.DNSRecord{Type: "TXT", Name: path, Content: val, TTL: ttl})
+		} else if old.Content != val {
+			// Entry already exists, only change its content.
+			log.Info(fmt.Sprintf("Updating %s from %q to %q", path, old.Content, val))
+			old.Content = val
+			err = c.UpdateDNSRecord(c.zoneID, old.ID, old)
+		} else {
+			log.Info(fmt.Sprintf("Skipping %s = %q", path, val))
+		}
+		if err != nil {
+			return fmt.Errorf("failed to publish %s: %v", path, err)
+		}
+	}
+
+	// Iterate over the old records and delete anything stale.
+	for path, entry := range existing {
+		if _, ok := records[path]; ok {
+			continue
+		}
+		// Stale entry, nuke it.
+		log.Info(fmt.Sprintf("Deleting %s = %q", path, entry.Content))
+		if err := c.DeleteDNSRecord(c.zoneID, entry.ID); err != nil {
+			return fmt.Errorf("failed to delete %s: %v", path, err)
+		}
+	}
+	return nil
+}
diff --git a/cmd/devp2p/dnscmd.go b/cmd/devp2p/dnscmd.go
new file mode 100644
index 000000000..74d70d3aa
--- /dev/null
+++ b/cmd/devp2p/dnscmd.go
@@ -0,0 +1,358 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"crypto/ecdsa"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/ethereum/go-ethereum/accounts/keystore"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/console"
+	"github.com/ethereum/go-ethereum/p2p/dnsdisc"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	cli "gopkg.in/urfave/cli.v1"
+)
+
+var (
+	dnsCommand = cli.Command{
+		Name:  "dns",
+		Usage: "DNS Discovery Commands",
+		Subcommands: []cli.Command{
+			dnsSyncCommand,
+			dnsSignCommand,
+			dnsTXTCommand,
+			dnsCloudflareCommand,
+		},
+	}
+	dnsSyncCommand = cli.Command{
+		Name:      "sync",
+		Usage:     "Download a DNS discovery tree",
+		ArgsUsage: "<url> [ <directory> ]",
+		Action:    dnsSync,
+		Flags:     []cli.Flag{dnsTimeoutFlag},
+	}
+	dnsSignCommand = cli.Command{
+		Name:      "sign",
+		Usage:     "Sign a DNS discovery tree",
+		ArgsUsage: "<tree-directory> <key-file>",
+		Action:    dnsSign,
+		Flags:     []cli.Flag{dnsDomainFlag, dnsSeqFlag},
+	}
+	dnsTXTCommand = cli.Command{
+		Name:      "to-txt",
+		Usage:     "Create a DNS TXT records for a discovery tree",
+		ArgsUsage: "<tree-directory> <output-file>",
+		Action:    dnsToTXT,
+	}
+	dnsCloudflareCommand = cli.Command{
+		Name:      "to-cloudflare",
+		Usage:     "Deploy DNS TXT records to cloudflare",
+		ArgsUsage: "<tree-directory>",
+		Action:    dnsToCloudflare,
+		Flags:     []cli.Flag{cloudflareTokenFlag, cloudflareZoneIDFlag},
+	}
+)
+
+var (
+	dnsTimeoutFlag = cli.DurationFlag{
+		Name:  "timeout",
+		Usage: "Timeout for DNS lookups",
+	}
+	dnsDomainFlag = cli.StringFlag{
+		Name:  "domain",
+		Usage: "Domain name of the tree",
+	}
+	dnsSeqFlag = cli.UintFlag{
+		Name:  "seq",
+		Usage: "New sequence number of the tree",
+	}
+)
+
+// dnsSync performs dnsSyncCommand.
+func dnsSync(ctx *cli.Context) error {
+	var (
+		c      = dnsClient(ctx)
+		url    = ctx.Args().Get(0)
+		outdir = ctx.Args().Get(1)
+	)
+	domain, _, err := dnsdisc.ParseURL(url)
+	if err != nil {
+		return err
+	}
+	if outdir == "" {
+		outdir = domain
+	}
+
+	t, err := c.SyncTree(url)
+	if err != nil {
+		return err
+	}
+	def := treeToDefinition(url, t)
+	def.Meta.LastModified = time.Now()
+	writeTreeDefinition(outdir, def)
+	return nil
+}
+
+func dnsSign(ctx *cli.Context) error {
+	if ctx.NArg() < 2 {
+		return fmt.Errorf("need tree definition directory and key file as arguments")
+	}
+	var (
+		defdir  = ctx.Args().Get(0)
+		keyfile = ctx.Args().Get(1)
+		def     = loadTreeDefinition(defdir)
+		domain  = directoryName(defdir)
+	)
+	if def.Meta.URL != "" {
+		d, _, err := dnsdisc.ParseURL(def.Meta.URL)
+		if err != nil {
+			return fmt.Errorf("invalid 'url' field: %v", err)
+		}
+		domain = d
+	}
+	if ctx.IsSet(dnsDomainFlag.Name) {
+		domain = ctx.String(dnsDomainFlag.Name)
+	}
+	if ctx.IsSet(dnsSeqFlag.Name) {
+		def.Meta.Seq = ctx.Uint(dnsSeqFlag.Name)
+	} else {
+		def.Meta.Seq++ // Auto-bump sequence number if not supplied via flag.
+	}
+	t, err := dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links)
+	if err != nil {
+		return err
+	}
+
+	key := loadSigningKey(keyfile)
+	url, err := t.Sign(key, domain)
+	if err != nil {
+		return fmt.Errorf("can't sign: %v", err)
+	}
+
+	def = treeToDefinition(url, t)
+	def.Meta.LastModified = time.Now()
+	writeTreeDefinition(defdir, def)
+	return nil
+}
+
+func directoryName(dir string) string {
+	abs, err := filepath.Abs(dir)
+	if err != nil {
+		exit(err)
+	}
+	return filepath.Base(abs)
+}
+
+// dnsToTXT peforms dnsTXTCommand.
+func dnsToTXT(ctx *cli.Context) error {
+	if ctx.NArg() < 1 {
+		return fmt.Errorf("need tree definition directory as argument")
+	}
+	output := ctx.Args().Get(1)
+	if output == "" {
+		output = "-" // default to stdout
+	}
+	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
+	if err != nil {
+		return err
+	}
+	writeTXTJSON(output, t.ToTXT(domain))
+	return nil
+}
+
+// dnsToCloudflare peforms dnsCloudflareCommand.
+func dnsToCloudflare(ctx *cli.Context) error {
+	if ctx.NArg() < 1 {
+		return fmt.Errorf("need tree definition directory as argument")
+	}
+	domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
+	if err != nil {
+		return err
+	}
+	client := newCloudflareClient(ctx)
+	return client.deploy(domain, t)
+}
+
+// loadSigningKey loads a private key in Ethereum keystore format.
+func loadSigningKey(keyfile string) *ecdsa.PrivateKey {
+	keyjson, err := ioutil.ReadFile(keyfile)
+	if err != nil {
+		exit(fmt.Errorf("failed to read the keyfile at '%s': %v", keyfile, err))
+	}
+	password, _ := console.Stdin.PromptPassword("Please enter the password for '" + keyfile + "': ")
+	key, err := keystore.DecryptKey(keyjson, password)
+	if err != nil {
+		exit(fmt.Errorf("error decrypting key: %v", err))
+	}
+	return key.PrivateKey
+}
+
+// dnsClient configures the DNS discovery client from command line flags.
+func dnsClient(ctx *cli.Context) *dnsdisc.Client {
+	var cfg dnsdisc.Config
+	if commandHasFlag(ctx, dnsTimeoutFlag) {
+		cfg.Timeout = ctx.Duration(dnsTimeoutFlag.Name)
+	}
+	c, _ := dnsdisc.NewClient(cfg) // cannot fail because no URLs given
+	return c
+}
+
+// There are two file formats for DNS node trees on disk:
+//
+// The 'TXT' format is a single JSON file containing DNS TXT records
+// as a JSON object where the keys are names and the values are objects
+// containing the value of the record.
+//
+// The 'definition' format is a directory containing two files:
+//
+//      enrtree-info.json    -- contains sequence number & links to other trees
+//      nodes.json           -- contains the nodes as a JSON array.
+//
+// This format exists because it's convenient to edit. nodes.json can be generated
+// in multiple ways: it may be written by a DHT crawler or compiled by a human.
+
+type dnsDefinition struct {
+	Meta  dnsMetaJSON
+	Nodes []*enode.Node
+}
+
+type dnsMetaJSON struct {
+	URL          string    `json:"url,omitempty"`
+	Seq          uint      `json:"seq"`
+	Sig          string    `json:"signature,omitempty"`
+	Links        []string  `json:"links"`
+	LastModified time.Time `json:"lastModified"`
+}
+
+func treeToDefinition(url string, t *dnsdisc.Tree) *dnsDefinition {
+	meta := dnsMetaJSON{
+		URL:   url,
+		Seq:   t.Seq(),
+		Sig:   t.Signature(),
+		Links: t.Links(),
+	}
+	if meta.Links == nil {
+		meta.Links = []string{}
+	}
+	return &dnsDefinition{Meta: meta, Nodes: t.Nodes()}
+}
+
+// loadTreeDefinition loads a directory in 'definition' format.
+func loadTreeDefinition(directory string) *dnsDefinition {
+	metaFile, nodesFile := treeDefinitionFiles(directory)
+	var def dnsDefinition
+	err := common.LoadJSON(metaFile, &def.Meta)
+	if err != nil && !os.IsNotExist(err) {
+		exit(err)
+	}
+	if def.Meta.Links == nil {
+		def.Meta.Links = []string{}
+	}
+	// Check link syntax.
+	for _, link := range def.Meta.Links {
+		if _, _, err := dnsdisc.ParseURL(link); err != nil {
+			exit(fmt.Errorf("invalid link %q: %v", link, err))
+		}
+	}
+	// Check/convert nodes.
+	nodes := loadNodesJSON(nodesFile)
+	if err := nodes.verify(); err != nil {
+		exit(err)
+	}
+	def.Nodes = nodes.nodes()
+	return &def
+}
+
+// loadTreeDefinitionForExport loads a DNS tree and ensures it is signed.
+func loadTreeDefinitionForExport(dir string) (domain string, t *dnsdisc.Tree, err error) {
+	metaFile, _ := treeDefinitionFiles(dir)
+	def := loadTreeDefinition(dir)
+	if def.Meta.URL == "" {
+		return "", nil, fmt.Errorf("missing 'url' field in %v", metaFile)
+	}
+	domain, pubkey, err := dnsdisc.ParseURL(def.Meta.URL)
+	if err != nil {
+		return "", nil, fmt.Errorf("invalid 'url' field in %v: %v", metaFile, err)
+	}
+	if t, err = dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links); err != nil {
+		return "", nil, err
+	}
+	if err := ensureValidTreeSignature(t, pubkey, def.Meta.Sig); err != nil {
+		return "", nil, err
+	}
+	return domain, t, nil
+}
+
+// ensureValidTreeSignature checks that sig is valid for tree and assigns it as the
+// tree's signature if valid.
+func ensureValidTreeSignature(t *dnsdisc.Tree, pubkey *ecdsa.PublicKey, sig string) error {
+	if sig == "" {
+		return fmt.Errorf("missing signature, run 'devp2p dns sign' first")
+	}
+	if err := t.SetSignature(pubkey, sig); err != nil {
+		return fmt.Errorf("invalid signature on tree, run 'devp2p dns sign' to update it")
+	}
+	return nil
+}
+
+// writeTreeDefinition writes a DNS node tree definition to the given directory.
+func writeTreeDefinition(directory string, def *dnsDefinition) {
+	metaJSON, err := json.MarshalIndent(&def.Meta, "", jsonIndent)
+	if err != nil {
+		exit(err)
+	}
+	// Convert nodes.
+	nodes := make(nodeSet, len(def.Nodes))
+	nodes.add(def.Nodes...)
+	// Write.
+	if err := os.Mkdir(directory, 0744); err != nil && !os.IsExist(err) {
+		exit(err)
+	}
+	metaFile, nodesFile := treeDefinitionFiles(directory)
+	writeNodesJSON(nodesFile, nodes)
+	if err := ioutil.WriteFile(metaFile, metaJSON, 0644); err != nil {
+		exit(err)
+	}
+}
+
+func treeDefinitionFiles(directory string) (string, string) {
+	meta := filepath.Join(directory, "enrtree-info.json")
+	nodes := filepath.Join(directory, "nodes.json")
+	return meta, nodes
+}
+
+// writeTXTJSON writes TXT records in JSON format.
+func writeTXTJSON(file string, txt map[string]string) {
+	txtJSON, err := json.MarshalIndent(txt, "", jsonIndent)
+	if err != nil {
+		exit(err)
+	}
+	if file == "-" {
+		os.Stdout.Write(txtJSON)
+		fmt.Println()
+		return
+	}
+	if err := ioutil.WriteFile(file, txtJSON, 0644); err != nil {
+		exit(err)
+	}
+}
diff --git a/cmd/devp2p/main.go b/cmd/devp2p/main.go
index 4532ab968..c88fe6f61 100644
--- a/cmd/devp2p/main.go
+++ b/cmd/devp2p/main.go
@@ -20,8 +20,10 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"sort"
 
 	"github.com/ethereum/go-ethereum/internal/debug"
+	"github.com/ethereum/go-ethereum/p2p/enode"
 	"github.com/ethereum/go-ethereum/params"
 	"gopkg.in/urfave/cli.v1"
 )
@@ -57,12 +59,38 @@ func init() {
 	app.Commands = []cli.Command{
 		enrdumpCommand,
 		discv4Command,
+		dnsCommand,
 	}
 }
 
 func main() {
-	if err := app.Run(os.Args); err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		os.Exit(1)
+	exit(app.Run(os.Args))
+}
+
+// commandHasFlag returns true if the current command supports the given flag.
+func commandHasFlag(ctx *cli.Context, flag cli.Flag) bool {
+	flags := ctx.FlagNames()
+	sort.Strings(flags)
+	i := sort.SearchStrings(flags, flag.GetName())
+	return i != len(flags) && flags[i] == flag.GetName()
+}
+
+// getNodeArg handles the common case of a single node descriptor argument.
+func getNodeArg(ctx *cli.Context) *enode.Node {
+	if ctx.NArg() != 1 {
+		exit("missing node as command-line argument")
+	}
+	n, err := parseNode(ctx.Args()[0])
+	if err != nil {
+		exit(err)
+	}
+	return n
+}
+
+func exit(err interface{}) {
+	if err == nil {
+		os.Exit(0)
 	}
+	fmt.Fprintln(os.Stderr, err)
+	os.Exit(1)
 }
diff --git a/cmd/devp2p/nodeset.go b/cmd/devp2p/nodeset.go
new file mode 100644
index 000000000..a4a05016e
--- /dev/null
+++ b/cmd/devp2p/nodeset.go
@@ -0,0 +1,87 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"sort"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+const jsonIndent = "    "
+
+// nodeSet is the nodes.json file format. It holds a set of node records
+// as a JSON object.
+type nodeSet map[enode.ID]nodeJSON
+
+type nodeJSON struct {
+	Seq uint64      `json:"seq"`
+	N   *enode.Node `json:"record"`
+}
+
+func loadNodesJSON(file string) nodeSet {
+	var nodes nodeSet
+	if err := common.LoadJSON(file, &nodes); err != nil {
+		exit(err)
+	}
+	return nodes
+}
+
+func writeNodesJSON(file string, nodes nodeSet) {
+	nodesJSON, err := json.MarshalIndent(nodes, "", jsonIndent)
+	if err != nil {
+		exit(err)
+	}
+	if err := ioutil.WriteFile(file, nodesJSON, 0644); err != nil {
+		exit(err)
+	}
+}
+
+func (ns nodeSet) nodes() []*enode.Node {
+	result := make([]*enode.Node, 0, len(ns))
+	for _, n := range ns {
+		result = append(result, n.N)
+	}
+	// Sort by ID.
+	sort.Slice(result, func(i, j int) bool {
+		return bytes.Compare(result[i].ID().Bytes(), result[j].ID().Bytes()) < 0
+	})
+	return result
+}
+
+func (ns nodeSet) add(nodes ...*enode.Node) {
+	for _, n := range nodes {
+		ns[n.ID()] = nodeJSON{Seq: n.Seq(), N: n}
+	}
+}
+
+func (ns nodeSet) verify() error {
+	for id, n := range ns {
+		if n.N.ID() != id {
+			return fmt.Errorf("invalid node %v: ID does not match ID %v in record", id, n.N.ID())
+		}
+		if n.N.Seq() != n.Seq {
+			return fmt.Errorf("invalid node %v: 'seq' does not match seq %d from record", id, n.N.Seq())
+		}
+	}
+	return nil
+}
diff --git a/p2p/dnsdisc/client.go b/p2p/dnsdisc/client.go
new file mode 100644
index 000000000..d4ed08ecb
--- /dev/null
+++ b/p2p/dnsdisc/client.go
@@ -0,0 +1,260 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package dnsdisc
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"math/rand"
+	"net"
+	"strings"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+	lru "github.com/hashicorp/golang-lru"
+)
+
+// Client discovers nodes by querying DNS servers.
+type Client struct {
+	cfg       Config
+	clock     mclock.Clock
+	linkCache linkCache
+	trees     map[string]*clientTree
+
+	entries *lru.Cache
+}
+
+// Config holds configuration options for the client.
+type Config struct {
+	Timeout         time.Duration      // timeout used for DNS lookups (default 5s)
+	RecheckInterval time.Duration      // time between tree root update checks (default 30min)
+	CacheLimit      int                // maximum number of cached records (default 1000)
+	ValidSchemes    enr.IdentityScheme // acceptable ENR identity schemes (default enode.ValidSchemes)
+	Resolver        Resolver           // the DNS resolver to use (defaults to system DNS)
+	Logger          log.Logger         // destination of client log messages (defaults to root logger)
+}
+
+// Resolver is a DNS resolver that can query TXT records.
+type Resolver interface {
+	LookupTXT(ctx context.Context, domain string) ([]string, error)
+}
+
+func (cfg Config) withDefaults() Config {
+	const (
+		defaultTimeout = 5 * time.Second
+		defaultRecheck = 30 * time.Minute
+		defaultCache   = 1000
+	)
+	if cfg.Timeout == 0 {
+		cfg.Timeout = defaultTimeout
+	}
+	if cfg.RecheckInterval == 0 {
+		cfg.RecheckInterval = defaultRecheck
+	}
+	if cfg.CacheLimit == 0 {
+		cfg.CacheLimit = defaultCache
+	}
+	if cfg.ValidSchemes == nil {
+		cfg.ValidSchemes = enode.ValidSchemes
+	}
+	if cfg.Resolver == nil {
+		cfg.Resolver = new(net.Resolver)
+	}
+	if cfg.Logger == nil {
+		cfg.Logger = log.Root()
+	}
+	return cfg
+}
+
+// NewClient creates a client.
+func NewClient(cfg Config, urls ...string) (*Client, error) {
+	c := &Client{
+		cfg:   cfg.withDefaults(),
+		clock: mclock.System{},
+		trees: make(map[string]*clientTree),
+	}
+	var err error
+	if c.entries, err = lru.New(c.cfg.CacheLimit); err != nil {
+		return nil, err
+	}
+	for _, url := range urls {
+		if err := c.AddTree(url); err != nil {
+			return nil, err
+		}
+	}
+	return c, nil
+}
+
+// SyncTree downloads the entire node tree at the given URL. This doesn't add the tree for
+// later use, but any previously-synced entries are reused.
+func (c *Client) SyncTree(url string) (*Tree, error) {
+	le, err := parseURL(url)
+	if err != nil {
+		return nil, fmt.Errorf("invalid enrtree URL: %v", err)
+	}
+	ct := newClientTree(c, le)
+	t := &Tree{entries: make(map[string]entry)}
+	if err := ct.syncAll(t.entries); err != nil {
+		return nil, err
+	}
+	t.root = ct.root
+	return t, nil
+}
+
+// AddTree adds a enrtree:// URL to crawl.
+func (c *Client) AddTree(url string) error {
+	le, err := parseURL(url)
+	if err != nil {
+		return fmt.Errorf("invalid enrtree URL: %v", err)
+	}
+	ct, err := c.ensureTree(le)
+	if err != nil {
+		return err
+	}
+	c.linkCache.add(ct)
+	return nil
+}
+
+func (c *Client) ensureTree(le *linkEntry) (*clientTree, error) {
+	if tree, ok := c.trees[le.domain]; ok {
+		if !tree.matchPubkey(le.pubkey) {
+			return nil, fmt.Errorf("conflicting public keys for domain %q", le.domain)
+		}
+		return tree, nil
+	}
+	ct := newClientTree(c, le)
+	c.trees[le.domain] = ct
+	return ct, nil
+}
+
+// RandomNode retrieves the next random node.
+func (c *Client) RandomNode(ctx context.Context) *enode.Node {
+	for {
+		ct := c.randomTree()
+		if ct == nil {
+			return nil
+		}
+		n, err := ct.syncRandom(ctx)
+		if err != nil {
+			if err == ctx.Err() {
+				return nil // context canceled.
+			}
+			c.cfg.Logger.Debug("Error in DNS random node sync", "tree", ct.loc.domain, "err", err)
+			continue
+		}
+		if n != nil {
+			return n
+		}
+	}
+}
+
+// randomTree returns a random tree.
+func (c *Client) randomTree() *clientTree {
+	if !c.linkCache.valid() {
+		c.gcTrees()
+	}
+	limit := rand.Intn(len(c.trees))
+	for _, ct := range c.trees {
+		if limit == 0 {
+			return ct
+		}
+		limit--
+	}
+	return nil
+}
+
+// gcTrees rebuilds the 'trees' map.
+func (c *Client) gcTrees() {
+	trees := make(map[string]*clientTree)
+	for t := range c.linkCache.all() {
+		trees[t.loc.domain] = t
+	}
+	c.trees = trees
+}
+
+// resolveRoot retrieves a root entry via DNS.
+func (c *Client) resolveRoot(ctx context.Context, loc *linkEntry) (rootEntry, error) {
+	txts, err := c.cfg.Resolver.LookupTXT(ctx, loc.domain)
+	c.cfg.Logger.Trace("Updating DNS discovery root", "tree", loc.domain, "err", err)
+	if err != nil {
+		return rootEntry{}, err
+	}
+	for _, txt := range txts {
+		if strings.HasPrefix(txt, rootPrefix) {
+			return parseAndVerifyRoot(txt, loc)
+		}
+	}
+	return rootEntry{}, nameError{loc.domain, errNoRoot}
+}
+
+func parseAndVerifyRoot(txt string, loc *linkEntry) (rootEntry, error) {
+	e, err := parseRoot(txt)
+	if err != nil {
+		return e, err
+	}
+	if !e.verifySignature(loc.pubkey) {
+		return e, entryError{typ: "root", err: errInvalidSig}
+	}
+	return e, nil
+}
+
+// resolveEntry retrieves an entry from the cache or fetches it from the network
+// if it isn't cached.
+func (c *Client) resolveEntry(ctx context.Context, domain, hash string) (entry, error) {
+	cacheKey := truncateHash(hash)
+	if e, ok := c.entries.Get(cacheKey); ok {
+		return e.(entry), nil
+	}
+	e, err := c.doResolveEntry(ctx, domain, hash)
+	if err != nil {
+		return nil, err
+	}
+	c.entries.Add(cacheKey, e)
+	return e, nil
+}
+
+// doResolveEntry fetches an entry via DNS.
+func (c *Client) doResolveEntry(ctx context.Context, domain, hash string) (entry, error) {
+	wantHash, err := b32format.DecodeString(hash)
+	if err != nil {
+		return nil, fmt.Errorf("invalid base32 hash")
+	}
+	name := hash + "." + domain
+	txts, err := c.cfg.Resolver.LookupTXT(ctx, hash+"."+domain)
+	c.cfg.Logger.Trace("DNS discovery lookup", "name", name, "err", err)
+	if err != nil {
+		return nil, err
+	}
+	for _, txt := range txts {
+		e, err := parseEntry(txt, c.cfg.ValidSchemes)
+		if err == errUnknownEntry {
+			continue
+		}
+		if !bytes.HasPrefix(crypto.Keccak256([]byte(txt)), wantHash) {
+			err = nameError{name, errHashMismatch}
+		} else if err != nil {
+			err = nameError{name, err}
+		}
+		return e, err
+	}
+	return nil, nameError{name, errNoEntry}
+}
diff --git a/p2p/dnsdisc/client_test.go b/p2p/dnsdisc/client_test.go
new file mode 100644
index 000000000..7e3a0f482
--- /dev/null
+++ b/p2p/dnsdisc/client_test.go
@@ -0,0 +1,306 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package dnsdisc
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"math/rand"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/davecgh/go-spew/spew"
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/internal/testlog"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+)
+
+const (
+	signingKeySeed = 0x111111
+	nodesSeed1     = 0x2945237
+	nodesSeed2     = 0x4567299
+)
+
+func TestClientSyncTree(t *testing.T) {
+	r := mapResolver{
+		"3CA2MBMUQ55ZCT74YEEQLANJDI.n": "enr=-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI=",
+		"53HBTPGGZ4I76UEPCNQGZWIPTQ.n": "enr=-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA=",
+		"BG7SVUBUAJ3UAWD2ATEBLMRNEE.n": "enrtree=53HBTPGGZ4I76UEPCNQGZWIPTQ,3CA2MBMUQ55ZCT74YEEQLANJDI,HNHR6UTVZF5TJKK3FV27ZI76P4",
+		"HNHR6UTVZF5TJKK3FV27ZI76P4.n": "enr=-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o=",
+		"JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
+		"n":                            "enrtree-root=v1 e=BG7SVUBUAJ3UAWD2ATEBLMRNEE l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=1 sig=gacuU0nTy9duIdu1IFDyF5Lv9CFHqHiNcj91n0frw70tZo3tZZsCVkE3j1ILYyVOHRLWGBmawo_SEkThZ9PgcQE=",
+	}
+	var (
+		wantNodes = testNodes(0x29452, 3)
+		wantLinks = []string{"enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org"}
+		wantSeq   = uint(1)
+	)
+
+	c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
+	stree, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
+	if err != nil {
+		t.Fatal("sync error:", err)
+	}
+	if !reflect.DeepEqual(sortByID(stree.Nodes()), sortByID(wantNodes)) {
+		t.Errorf("wrong nodes in synced tree:\nhave %v\nwant %v", spew.Sdump(stree.Nodes()), spew.Sdump(wantNodes))
+	}
+	if !reflect.DeepEqual(stree.Links(), wantLinks) {
+		t.Errorf("wrong links in synced tree: %v", stree.Links())
+	}
+	if stree.Seq() != wantSeq {
+		t.Errorf("synced tree has wrong seq: %d", stree.Seq())
+	}
+	if len(c.trees) > 0 {
+		t.Errorf("tree from SyncTree added to client")
+	}
+}
+
+// In this test, syncing the tree fails because it contains an invalid ENR entry.
+func TestClientSyncTreeBadNode(t *testing.T) {
+	r := mapResolver{
+		"n":                            "enrtree-root=v1 e=ZFJZDQKSOMJRYYQSZKJZC54HCF l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=WEy8JTZ2dHmXM2qeBZ7D2ECK7SGbnurl1ge_S_5GQBAqnADk0gLTcg8Lm5QNqLHZjJKGAb443p996idlMcBqEQA=",
+		"JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
+		"ZFJZDQKSOMJRYYQSZKJZC54HCF.n": "enr=gggggggggggggg=",
+	}
+
+	c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
+	_, err := c.SyncTree("enrtree://APFGGTFOBVE2ZNAB3CSMNNX6RRK3ODIRLP2AA5U4YFAA6MSYZUYTQ@n")
+	wantErr := nameError{name: "ZFJZDQKSOMJRYYQSZKJZC54HCF.n", err: entryError{typ: "enr", err: errInvalidENR}}
+	if err != wantErr {
+		t.Fatalf("expected sync error %q, got %q", wantErr, err)
+	}
+}
+
+// This test checks that RandomNode hits all entries.
+func TestClientRandomNode(t *testing.T) {
+	nodes := testNodes(nodesSeed1, 30)
+	tree, url := makeTestTree("n", nodes, nil)
+	r := mapResolver(tree.ToTXT("n"))
+	c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
+	if err := c.AddTree(url); err != nil {
+		t.Fatal(err)
+	}
+
+	checkRandomNode(t, c, nodes)
+}
+
+// This test checks that RandomNode traverses linked trees as well as explicitly added trees.
+func TestClientRandomNodeLinks(t *testing.T) {
+	nodes := testNodes(nodesSeed1, 40)
+	tree1, url1 := makeTestTree("t1", nodes[:10], nil)
+	tree2, url2 := makeTestTree("t2", nodes[10:], []string{url1})
+	cfg := Config{
+		Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")),
+		Logger:   testlog.Logger(t, log.LvlTrace),
+	}
+	c, _ := NewClient(cfg)
+	if err := c.AddTree(url2); err != nil {
+		t.Fatal(err)
+	}
+
+	checkRandomNode(t, c, nodes)
+}
+
+// This test verifies that RandomNode re-checks the root of the tree to catch
+// updates to nodes.
+func TestClientRandomNodeUpdates(t *testing.T) {
+	var (
+		clock    = new(mclock.Simulated)
+		nodes    = testNodes(nodesSeed1, 30)
+		resolver = newMapResolver()
+		cfg      = Config{
+			Resolver:        resolver,
+			Logger:          testlog.Logger(t, log.LvlTrace),
+			RecheckInterval: 20 * time.Minute,
+		}
+		c, _ = NewClient(cfg)
+	)
+	c.clock = clock
+	tree1, url := makeTestTree("n", nodes[:25], nil)
+
+	// Sync the original tree.
+	resolver.add(tree1.ToTXT("n"))
+	c.AddTree(url)
+	checkRandomNode(t, c, nodes[:25])
+
+	// Update some nodes and ensure RandomNode returns the new nodes as well.
+	keys := testKeys(nodesSeed1, len(nodes))
+	for i, n := range nodes[:len(nodes)/2] {
+		r := n.Record()
+		r.Set(enr.IP{127, 0, 0, 1})
+		r.SetSeq(55)
+		enode.SignV4(r, keys[i])
+		n2, _ := enode.New(enode.ValidSchemes, r)
+		nodes[i] = n2
+	}
+	tree2, _ := makeTestTree("n", nodes, nil)
+	clock.Run(cfg.RecheckInterval + 1*time.Second)
+	resolver.clear()
+	resolver.add(tree2.ToTXT("n"))
+	checkRandomNode(t, c, nodes)
+}
+
+// This test verifies that RandomNode re-checks the root of the tree to catch
+// updates to links.
+func TestClientRandomNodeLinkUpdates(t *testing.T) {
+	var (
+		clock    = new(mclock.Simulated)
+		nodes    = testNodes(nodesSeed1, 30)
+		resolver = newMapResolver()
+		cfg      = Config{
+			Resolver:        resolver,
+			Logger:          testlog.Logger(t, log.LvlTrace),
+			RecheckInterval: 20 * time.Minute,
+		}
+		c, _ = NewClient(cfg)
+	)
+	c.clock = clock
+	tree3, url3 := makeTestTree("t3", nodes[20:30], nil)
+	tree2, url2 := makeTestTree("t2", nodes[10:20], nil)
+	tree1, url1 := makeTestTree("t1", nodes[0:10], []string{url2})
+	resolver.add(tree1.ToTXT("t1"))
+	resolver.add(tree2.ToTXT("t2"))
+	resolver.add(tree3.ToTXT("t3"))
+
+	// Sync tree1 using RandomNode.
+	c.AddTree(url1)
+	checkRandomNode(t, c, nodes[:20])
+
+	// Add link to tree3, remove link to tree2.
+	tree1, _ = makeTestTree("t1", nodes[:10], []string{url3})
+	resolver.add(tree1.ToTXT("t1"))
+	clock.Run(cfg.RecheckInterval + 1*time.Second)
+	t.Log("tree1 updated")
+
+	var wantNodes []*enode.Node
+	wantNodes = append(wantNodes, tree1.Nodes()...)
+	wantNodes = append(wantNodes, tree3.Nodes()...)
+	checkRandomNode(t, c, wantNodes)
+
+	// Check that linked trees are GCed when they're no longer referenced.
+	if len(c.trees) != 2 {
+		t.Errorf("client knows %d trees, want 2", len(c.trees))
+	}
+}
+
+func checkRandomNode(t *testing.T, c *Client, wantNodes []*enode.Node) {
+	t.Helper()
+
+	var (
+		want     = make(map[enode.ID]*enode.Node)
+		maxCalls = len(wantNodes) * 2
+		calls    = 0
+		ctx      = context.Background()
+	)
+	for _, n := range wantNodes {
+		want[n.ID()] = n
+	}
+	for ; len(want) > 0 && calls < maxCalls; calls++ {
+		n := c.RandomNode(ctx)
+		if n == nil {
+			t.Fatalf("RandomNode returned nil (call %d)", calls)
+		}
+		delete(want, n.ID())
+	}
+	t.Logf("checkRandomNode called RandomNode %d times to find %d nodes", calls, len(wantNodes))
+	for _, n := range want {
+		t.Errorf("RandomNode didn't discover node %v", n.ID())
+	}
+}
+
+func makeTestTree(domain string, nodes []*enode.Node, links []string) (*Tree, string) {
+	tree, err := MakeTree(1, nodes, links)
+	if err != nil {
+		panic(err)
+	}
+	url, err := tree.Sign(testKey(signingKeySeed), domain)
+	if err != nil {
+		panic(err)
+	}
+	return tree, url
+}
+
+// testKeys creates deterministic private keys for testing.
+func testKeys(seed int64, n int) []*ecdsa.PrivateKey {
+	rand := rand.New(rand.NewSource(seed))
+	keys := make([]*ecdsa.PrivateKey, n)
+	for i := 0; i < n; i++ {
+		key, err := ecdsa.GenerateKey(crypto.S256(), rand)
+		if err != nil {
+			panic("can't generate key: " + err.Error())
+		}
+		keys[i] = key
+	}
+	return keys
+}
+
+func testKey(seed int64) *ecdsa.PrivateKey {
+	return testKeys(seed, 1)[0]
+}
+
+func testNodes(seed int64, n int) []*enode.Node {
+	keys := testKeys(seed, n)
+	nodes := make([]*enode.Node, n)
+	for i, key := range keys {
+		record := new(enr.Record)
+		record.SetSeq(uint64(i))
+		enode.SignV4(record, key)
+		n, err := enode.New(enode.ValidSchemes, record)
+		if err != nil {
+			panic(err)
+		}
+		nodes[i] = n
+	}
+	return nodes
+}
+
+func testNode(seed int64) *enode.Node {
+	return testNodes(seed, 1)[0]
+}
+
+type mapResolver map[string]string
+
+func newMapResolver(maps ...map[string]string) mapResolver {
+	mr := make(mapResolver)
+	for _, m := range maps {
+		mr.add(m)
+	}
+	return mr
+}
+
+func (mr mapResolver) clear() {
+	for k := range mr {
+		delete(mr, k)
+	}
+}
+
+func (mr mapResolver) add(m map[string]string) {
+	for k, v := range m {
+		mr[k] = v
+	}
+}
+
+func (mr mapResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
+	if record, ok := mr[name]; ok {
+		return []string{record}, nil
+	}
+	return nil, nil
+}
diff --git a/p2p/dnsdisc/doc.go b/p2p/dnsdisc/doc.go
new file mode 100644
index 000000000..227467d08
--- /dev/null
+++ b/p2p/dnsdisc/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+// Package dnsdisc implements node discovery via DNS (EIP-1459).
+package dnsdisc
diff --git a/p2p/dnsdisc/error.go b/p2p/dnsdisc/error.go
new file mode 100644
index 000000000..e0998c735
--- /dev/null
+++ b/p2p/dnsdisc/error.go
@@ -0,0 +1,63 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package dnsdisc
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Entry parse errors.
+var (
+	errUnknownEntry = errors.New("unknown entry type")
+	errNoPubkey     = errors.New("missing public key")
+	errBadPubkey    = errors.New("invalid public key")
+	errInvalidENR   = errors.New("invalid node record")
+	errInvalidChild = errors.New("invalid child hash")
+	errInvalidSig   = errors.New("invalid base64 signature")
+	errSyntax       = errors.New("invalid syntax")
+)
+
+// Resolver/sync errors
+var (
+	errNoRoot        = errors.New("no valid root found")
+	errNoEntry       = errors.New("no valid tree entry found")
+	errHashMismatch  = errors.New("hash mismatch")
+	errENRInLinkTree = errors.New("enr entry in link tree")
+	errLinkInENRTree = errors.New("link entry in ENR tree")
+)
+
+type nameError struct {
+	name string
+	err  error
+}
+
+func (err nameError) Error() string {
+	if ee, ok := err.err.(entryError); ok {
+		return fmt.Sprintf("invalid %s entry at %s: %v", ee.typ, err.name, ee.err)
+	}
+	return err.name + ": " + err.err.Error()
+}
+
+type entryError struct {
+	typ string
+	err error
+}
+
+func (err entryError) Error() string {
+	return fmt.Sprintf("invalid %s entry: %v", err.typ, err.err)
+}
diff --git a/p2p/dnsdisc/sync.go b/p2p/dnsdisc/sync.go
new file mode 100644
index 000000000..bd5c8d023
--- /dev/null
+++ b/p2p/dnsdisc/sync.go
@@ -0,0 +1,277 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package dnsdisc
+
+import (
+	"context"
+	"crypto/ecdsa"
+	"math/rand"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+// clientTree is a full tree being synced.
+type clientTree struct {
+	c             *Client
+	loc           *linkEntry
+	root          *rootEntry
+	lastRootCheck mclock.AbsTime // last revalidation of root
+	enrs          *subtreeSync
+	links         *subtreeSync
+	linkCache     linkCache
+}
+
+func newClientTree(c *Client, loc *linkEntry) *clientTree {
+	ct := &clientTree{c: c, loc: loc}
+	ct.linkCache.self = ct
+	return ct
+}
+
+func (ct *clientTree) matchPubkey(key *ecdsa.PublicKey) bool {
+	return keysEqual(ct.loc.pubkey, key)
+}
+
+func keysEqual(k1, k2 *ecdsa.PublicKey) bool {
+	return k1.Curve == k2.Curve && k1.X.Cmp(k2.X) == 0 && k1.Y.Cmp(k2.Y) == 0
+}
+
+// syncAll retrieves all entries of the tree.
+func (ct *clientTree) syncAll(dest map[string]entry) error {
+	if err := ct.updateRoot(); err != nil {
+		return err
+	}
+	if err := ct.links.resolveAll(dest); err != nil {
+		return err
+	}
+	if err := ct.enrs.resolveAll(dest); err != nil {
+		return err
+	}
+	return nil
+}
+
+// syncRandom retrieves a single entry of the tree. The Node return value
+// is non-nil if the entry was a node.
+func (ct *clientTree) syncRandom(ctx context.Context) (*enode.Node, error) {
+	if ct.rootUpdateDue() {
+		if err := ct.updateRoot(); err != nil {
+			return nil, err
+		}
+	}
+	// Link tree sync has priority, run it to completion before syncing ENRs.
+	if !ct.links.done() {
+		err := ct.syncNextLink(ctx)
+		return nil, err
+	}
+
+	// Sync next random entry in ENR tree. Once every node has been visited, we simply
+	// start over. This is fine because entries are cached.
+	if ct.enrs.done() {
+		ct.enrs = newSubtreeSync(ct.c, ct.loc, ct.root.eroot, false)
+	}
+	return ct.syncNextRandomENR(ctx)
+}
+
+func (ct *clientTree) syncNextLink(ctx context.Context) error {
+	hash := ct.links.missing[0]
+	e, err := ct.links.resolveNext(ctx, hash)
+	if err != nil {
+		return err
+	}
+	ct.links.missing = ct.links.missing[1:]
+
+	if le, ok := e.(*linkEntry); ok {
+		lt, err := ct.c.ensureTree(le)
+		if err != nil {
+			return err
+		}
+		ct.linkCache.add(lt)
+	}
+	return nil
+}
+
+func (ct *clientTree) syncNextRandomENR(ctx context.Context) (*enode.Node, error) {
+	index := rand.Intn(len(ct.enrs.missing))
+	hash := ct.enrs.missing[index]
+	e, err := ct.enrs.resolveNext(ctx, hash)
+	if err != nil {
+		return nil, err
+	}
+	ct.enrs.missing = removeHash(ct.enrs.missing, index)
+	if ee, ok := e.(*enrEntry); ok {
+		return ee.node, nil
+	}
+	return nil, nil
+}
+
+func (ct *clientTree) String() string {
+	return ct.loc.url()
+}
+
+// removeHash removes the element at index from h.
+func removeHash(h []string, index int) []string {
+	if len(h) == 1 {
+		return nil
+	}
+	last := len(h) - 1
+	if index < last {
+		h[index] = h[last]
+		h[last] = ""
+	}
+	return h[:last]
+}
+
+// updateRoot ensures that the given tree has an up-to-date root.
+func (ct *clientTree) updateRoot() error {
+	ct.lastRootCheck = ct.c.clock.Now()
+	ctx, cancel := context.WithTimeout(context.Background(), ct.c.cfg.Timeout)
+	defer cancel()
+	root, err := ct.c.resolveRoot(ctx, ct.loc)
+	if err != nil {
+		return err
+	}
+	ct.root = &root
+
+	// Invalidate subtrees if changed.
+	if ct.links == nil || root.lroot != ct.links.root {
+		ct.links = newSubtreeSync(ct.c, ct.loc, root.lroot, true)
+		ct.linkCache.reset()
+	}
+	if ct.enrs == nil || root.eroot != ct.enrs.root {
+		ct.enrs = newSubtreeSync(ct.c, ct.loc, root.eroot, false)
+	}
+	return nil
+}
+
+// rootUpdateDue returns true when a root update is needed.
+func (ct *clientTree) rootUpdateDue() bool {
+	return ct.root == nil || time.Duration(ct.c.clock.Now()-ct.lastRootCheck) > ct.c.cfg.RecheckInterval
+}
+
+// subtreeSync is the sync of an ENR or link subtree.
+type subtreeSync struct {
+	c       *Client
+	loc     *linkEntry
+	root    string
+	missing []string // missing tree node hashes
+	link    bool     // true if this sync is for the link tree
+}
+
+func newSubtreeSync(c *Client, loc *linkEntry, root string, link bool) *subtreeSync {
+	return &subtreeSync{c, loc, root, []string{root}, link}
+}
+
+func (ts *subtreeSync) done() bool {
+	return len(ts.missing) == 0
+}
+
+func (ts *subtreeSync) resolveAll(dest map[string]entry) error {
+	for !ts.done() {
+		hash := ts.missing[0]
+		ctx, cancel := context.WithTimeout(context.Background(), ts.c.cfg.Timeout)
+		e, err := ts.resolveNext(ctx, hash)
+		cancel()
+		if err != nil {
+			return err
+		}
+		dest[hash] = e
+		ts.missing = ts.missing[1:]
+	}
+	return nil
+}
+
+func (ts *subtreeSync) resolveNext(ctx context.Context, hash string) (entry, error) {
+	e, err := ts.c.resolveEntry(ctx, ts.loc.domain, hash)
+	if err != nil {
+		return nil, err
+	}
+	switch e := e.(type) {
+	case *enrEntry:
+		if ts.link {
+			return nil, errENRInLinkTree
+		}
+	case *linkEntry:
+		if !ts.link {
+			return nil, errLinkInENRTree
+		}
+	case *subtreeEntry:
+		ts.missing = append(ts.missing, e.children...)
+	}
+	return e, nil
+}
+
+// linkCache tracks the links of a tree.
+type linkCache struct {
+	self    *clientTree
+	directM map[*clientTree]struct{} // direct links
+	allM    map[*clientTree]struct{} // direct & transitive links
+}
+
+// reset clears the cache.
+func (lc *linkCache) reset() {
+	lc.directM = nil
+	lc.allM = nil
+}
+
+// add adds a direct link to the cache.
+func (lc *linkCache) add(ct *clientTree) {
+	if lc.directM == nil {
+		lc.directM = make(map[*clientTree]struct{})
+	}
+	if _, ok := lc.directM[ct]; !ok {
+		lc.invalidate()
+	}
+	lc.directM[ct] = struct{}{}
+}
+
+// invalidate resets the cache of transitive links.
+func (lc *linkCache) invalidate() {
+	lc.allM = nil
+}
+
+// valid returns true when the cache of transitive links is up-to-date.
+func (lc *linkCache) valid() bool {
+	// Re-check validity of child caches to catch updates.
+	for ct := range lc.allM {
+		if ct != lc.self && !ct.linkCache.valid() {
+			lc.allM = nil
+			break
+		}
+	}
+	return lc.allM != nil
+}
+
+// all returns all trees reachable through the cache.
+func (lc *linkCache) all() map[*clientTree]struct{} {
+	if lc.valid() {
+		return lc.allM
+	}
+	// Remake lc.allM it by taking the union of all() across children.
+	m := make(map[*clientTree]struct{})
+	if lc.self != nil {
+		m[lc.self] = struct{}{}
+	}
+	for ct := range lc.directM {
+		m[ct] = struct{}{}
+		for lt := range ct.linkCache.all() {
+			m[lt] = struct{}{}
+		}
+	}
+	lc.allM = m
+	return m
+}
diff --git a/p2p/dnsdisc/tree.go b/p2p/dnsdisc/tree.go
new file mode 100644
index 000000000..855d4968c
--- /dev/null
+++ b/p2p/dnsdisc/tree.go
@@ -0,0 +1,384 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package dnsdisc
+
+import (
+	"bytes"
+	"crypto/ecdsa"
+	"encoding/base32"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/ethereum/go-ethereum/rlp"
+	"golang.org/x/crypto/sha3"
+)
+
+// Tree is a merkle tree of node records.
+type Tree struct {
+	root    *rootEntry
+	entries map[string]entry
+}
+
+// Sign signs the tree with the given private key and sets the sequence number.
+func (t *Tree) Sign(key *ecdsa.PrivateKey, domain string) (url string, err error) {
+	root := *t.root
+	sig, err := crypto.Sign(root.sigHash(), key)
+	if err != nil {
+		return "", err
+	}
+	root.sig = sig
+	t.root = &root
+	link := &linkEntry{domain, &key.PublicKey}
+	return link.url(), nil
+}
+
+// SetSignature verifies the given signature and assigns it as the tree's current
+// signature if valid.
+func (t *Tree) SetSignature(pubkey *ecdsa.PublicKey, signature string) error {
+	sig, err := b64format.DecodeString(signature)
+	if err != nil || len(sig) != crypto.SignatureLength {
+		return errInvalidSig
+	}
+	root := *t.root
+	root.sig = sig
+	if !root.verifySignature(pubkey) {
+		return errInvalidSig
+	}
+	t.root = &root
+	return nil
+}
+
+// Seq returns the sequence number of the tree.
+func (t *Tree) Seq() uint {
+	return t.root.seq
+}
+
+// Signature returns the signature of the tree.
+func (t *Tree) Signature() string {
+	return b64format.EncodeToString(t.root.sig)
+}
+
+// ToTXT returns all DNS TXT records required for the tree.
+func (t *Tree) ToTXT(domain string) map[string]string {
+	records := map[string]string{domain: t.root.String()}
+	for _, e := range t.entries {
+		sd := subdomain(e)
+		if domain != "" {
+			sd = sd + "." + domain
+		}
+		records[sd] = e.String()
+	}
+	return records
+}
+
+// Links returns all links contained in the tree.
+func (t *Tree) Links() []string {
+	var links []string
+	for _, e := range t.entries {
+		if le, ok := e.(*linkEntry); ok {
+			links = append(links, le.url())
+		}
+	}
+	return links
+}
+
+// Nodes returns all nodes contained in the tree.
+func (t *Tree) Nodes() []*enode.Node {
+	var nodes []*enode.Node
+	for _, e := range t.entries {
+		if ee, ok := e.(*enrEntry); ok {
+			nodes = append(nodes, ee.node)
+		}
+	}
+	return nodes
+}
+
+const (
+	hashAbbrev    = 16
+	maxChildren   = 300 / (hashAbbrev * (13 / 8))
+	minHashLength = 12
+	rootPrefix    = "enrtree-root=v1"
+)
+
+// MakeTree creates a tree containing the given nodes and links.
+func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) {
+	// Sort records by ID and ensure all nodes have a valid record.
+	records := make([]*enode.Node, len(nodes))
+	copy(records, nodes)
+	sortByID(records)
+	for _, n := range records {
+		if len(n.Record().Signature()) == 0 {
+			return nil, fmt.Errorf("can't add node %v: unsigned node record", n.ID())
+		}
+	}
+
+	// Create the leaf list.
+	enrEntries := make([]entry, len(records))
+	for i, r := range records {
+		enrEntries[i] = &enrEntry{r}
+	}
+	linkEntries := make([]entry, len(links))
+	for i, l := range links {
+		le, err := parseURL(l)
+		if err != nil {
+			return nil, err
+		}
+		linkEntries[i] = le
+	}
+
+	// Create intermediate nodes.
+	t := &Tree{entries: make(map[string]entry)}
+	eroot := t.build(enrEntries)
+	t.entries[subdomain(eroot)] = eroot
+	lroot := t.build(linkEntries)
+	t.entries[subdomain(lroot)] = lroot
+	t.root = &rootEntry{seq: seq, eroot: subdomain(eroot), lroot: subdomain(lroot)}
+	return t, nil
+}
+
+func (t *Tree) build(entries []entry) entry {
+	if len(entries) == 1 {
+		return entries[0]
+	}
+	if len(entries) <= maxChildren {
+		hashes := make([]string, len(entries))
+		for i, e := range entries {
+			hashes[i] = subdomain(e)
+			t.entries[hashes[i]] = e
+		}
+		return &subtreeEntry{hashes}
+	}
+	var subtrees []entry
+	for len(entries) > 0 {
+		n := maxChildren
+		if len(entries) < n {
+			n = len(entries)
+		}
+		sub := t.build(entries[:n])
+		entries = entries[n:]
+		subtrees = append(subtrees, sub)
+		t.entries[subdomain(sub)] = sub
+	}
+	return t.build(subtrees)
+}
+
+func sortByID(nodes []*enode.Node) []*enode.Node {
+	sort.Slice(nodes, func(i, j int) bool {
+		return bytes.Compare(nodes[i].ID().Bytes(), nodes[j].ID().Bytes()) < 0
+	})
+	return nodes
+}
+
+// Entry Types
+
+type entry interface {
+	fmt.Stringer
+}
+
+type (
+	rootEntry struct {
+		eroot string
+		lroot string
+		seq   uint
+		sig   []byte
+	}
+	subtreeEntry struct {
+		children []string
+	}
+	enrEntry struct {
+		node *enode.Node
+	}
+	linkEntry struct {
+		domain string
+		pubkey *ecdsa.PublicKey
+	}
+)
+
+// Entry Encoding
+
+var (
+	b32format = base32.StdEncoding.WithPadding(base32.NoPadding)
+	b64format = base64.URLEncoding
+)
+
+func subdomain(e entry) string {
+	h := sha3.NewLegacyKeccak256()
+	io.WriteString(h, e.String())
+	return b32format.EncodeToString(h.Sum(nil)[:16])
+}
+
+func (e *rootEntry) String() string {
+	return fmt.Sprintf(rootPrefix+" e=%s l=%s seq=%d sig=%s", e.eroot, e.lroot, e.seq, b64format.EncodeToString(e.sig))
+}
+
+func (e *rootEntry) sigHash() []byte {
+	h := sha3.NewLegacyKeccak256()
+	fmt.Fprintf(h, rootPrefix+" e=%s l=%s seq=%d", e.eroot, e.lroot, e.seq)
+	return h.Sum(nil)
+}
+
+func (e *rootEntry) verifySignature(pubkey *ecdsa.PublicKey) bool {
+	sig := e.sig[:crypto.RecoveryIDOffset] // remove recovery id
+	return crypto.VerifySignature(crypto.FromECDSAPub(pubkey), e.sigHash(), sig)
+}
+
+func (e *subtreeEntry) String() string {
+	return "enrtree=" + strings.Join(e.children, ",")
+}
+
+func (e *enrEntry) String() string {
+	enc, _ := rlp.EncodeToBytes(e.node.Record())
+	return "enr=" + b64format.EncodeToString(enc)
+}
+
+func (e *linkEntry) String() string {
+	return "enrtree-link=" + e.link()
+}
+
+func (e *linkEntry) url() string {
+	return "enrtree://" + e.link()
+}
+
+func (e *linkEntry) link() string {
+	return fmt.Sprintf("%s@%s", b32format.EncodeToString(crypto.CompressPubkey(e.pubkey)), e.domain)
+}
+
+// Entry Parsing
+
+func parseEntry(e string, validSchemes enr.IdentityScheme) (entry, error) {
+	switch {
+	case strings.HasPrefix(e, "enrtree-link="):
+		return parseLink(e[13:])
+	case strings.HasPrefix(e, "enrtree="):
+		return parseSubtree(e[8:])
+	case strings.HasPrefix(e, "enr="):
+		return parseENR(e[4:], validSchemes)
+	default:
+		return nil, errUnknownEntry
+	}
+}
+
+func parseRoot(e string) (rootEntry, error) {
+	var eroot, lroot, sig string
+	var seq uint
+	if _, err := fmt.Sscanf(e, rootPrefix+" e=%s l=%s seq=%d sig=%s", &eroot, &lroot, &seq, &sig); err != nil {
+		return rootEntry{}, entryError{"root", errSyntax}
+	}
+	if !isValidHash(eroot) || !isValidHash(lroot) {
+		return rootEntry{}, entryError{"root", errInvalidChild}
+	}
+	sigb, err := b64format.DecodeString(sig)
+	if err != nil || len(sigb) != crypto.SignatureLength {
+		return rootEntry{}, entryError{"root", errInvalidSig}
+	}
+	return rootEntry{eroot, lroot, seq, sigb}, nil
+}
+
+func parseLink(e string) (entry, error) {
+	pos := strings.IndexByte(e, '@')
+	if pos == -1 {
+		return nil, entryError{"link", errNoPubkey}
+	}
+	keystring, domain := e[:pos], e[pos+1:]
+	keybytes, err := b32format.DecodeString(keystring)
+	if err != nil {
+		return nil, entryError{"link", errBadPubkey}
+	}
+	key, err := crypto.DecompressPubkey(keybytes)
+	if err != nil {
+		return nil, entryError{"link", errBadPubkey}
+	}
+	return &linkEntry{domain, key}, nil
+}
+
+func parseSubtree(e string) (entry, error) {
+	if e == "" {
+		return &subtreeEntry{}, nil // empty entry is OK
+	}
+	hashes := make([]string, 0, strings.Count(e, ","))
+	for _, c := range strings.Split(e, ",") {
+		if !isValidHash(c) {
+			return nil, entryError{"subtree", errInvalidChild}
+		}
+		hashes = append(hashes, c)
+	}
+	return &subtreeEntry{hashes}, nil
+}
+
+func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) {
+	enc, err := b64format.DecodeString(e)
+	if err != nil {
+		return nil, entryError{"enr", errInvalidENR}
+	}
+	var rec enr.Record
+	if err := rlp.DecodeBytes(enc, &rec); err != nil {
+		return nil, entryError{"enr", err}
+	}
+	n, err := enode.New(validSchemes, &rec)
+	if err != nil {
+		return nil, entryError{"enr", err}
+	}
+	return &enrEntry{n}, nil
+}
+
+func isValidHash(s string) bool {
+	dlen := b32format.DecodedLen(len(s))
+	if dlen < minHashLength || dlen > 32 || strings.ContainsAny(s, "\n\r") {
+		return false
+	}
+	buf := make([]byte, 32)
+	_, err := b32format.Decode(buf, []byte(s))
+	return err == nil
+}
+
+// truncateHash truncates the given base32 hash string to the minimum acceptable length.
+func truncateHash(hash string) string {
+	maxLen := b32format.EncodedLen(minHashLength)
+	if len(hash) < maxLen {
+		panic(fmt.Errorf("dnsdisc: hash %q is too short", hash))
+	}
+	return hash[:maxLen]
+}
+
+// URL encoding
+
+// ParseURL parses an enrtree:// URL and returns its components.
+func ParseURL(url string) (domain string, pubkey *ecdsa.PublicKey, err error) {
+	le, err := parseURL(url)
+	if err != nil {
+		return "", nil, err
+	}
+	return le.domain, le.pubkey, nil
+}
+
+func parseURL(url string) (*linkEntry, error) {
+	const scheme = "enrtree://"
+	if !strings.HasPrefix(url, scheme) {
+		return nil, fmt.Errorf("wrong/missing scheme 'enrtree' in URL")
+	}
+	le, err := parseLink(url[len(scheme):])
+	if err != nil {
+		return nil, err.(entryError).err
+	}
+	return le.(*linkEntry), nil
+}
diff --git a/p2p/dnsdisc/tree_test.go b/p2p/dnsdisc/tree_test.go
new file mode 100644
index 000000000..e2fe96245
--- /dev/null
+++ b/p2p/dnsdisc/tree_test.go
@@ -0,0 +1,144 @@
+// Copyright 2018 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package dnsdisc
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/davecgh/go-spew/spew"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+func TestParseRoot(t *testing.T) {
+	tests := []struct {
+		input string
+		e     rootEntry
+		err   error
+	}{
+		{
+			input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=",
+			err:   entryError{"root", errSyntax},
+		},
+		{
+			input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=",
+			err:   entryError{"root", errInvalidSig},
+		},
+		{
+			input: "enrtree-root=v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE=",
+			e: rootEntry{
+				eroot: "QFT4PBCRX4XQCV3VUYJ6BTCEPU",
+				lroot: "JGUFMSAGI7KZYB3P7IZW4S5Y3A",
+				seq:   3,
+				sig:   hexutil.MustDecode("0xdc5997b95c296bc63b3acb594f1f4f21bd66b7c16b5bb5690ce16fe006860ac6761081e686b69685ee0dc588500e5c393237855d831b263b0f78a947ce62511101"),
+			},
+		},
+	}
+	for i, test := range tests {
+		e, err := parseRoot(test.input)
+		if !reflect.DeepEqual(e, test.e) {
+			t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
+		}
+		if err != test.err {
+			t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
+		}
+	}
+}
+
+func TestParseEntry(t *testing.T) {
+	testkey := testKey(signingKeySeed)
+	tests := []struct {
+		input string
+		e     entry
+		err   error
+	}{
+		// Subtrees:
+		{
+			input: "enrtree=1,2",
+			err:   entryError{"subtree", errInvalidChild},
+		},
+		{
+			input: "enrtree=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+			err:   entryError{"subtree", errInvalidChild},
+		},
+		{
+			input: "enrtree=",
+			e:     &subtreeEntry{},
+		},
+		{
+			input: "enrtree=AAAAAAAAAAAAAAAAAAAA",
+			e:     &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}},
+		},
+		{
+			input: "enrtree=AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB",
+			e:     &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}},
+		},
+		// Links
+		{
+			input: "enrtree-link=AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org",
+			e:     &linkEntry{"nodes.example.org", &testkey.PublicKey},
+		},
+		{
+			input: "enrtree-link=nodes.example.org",
+			err:   entryError{"link", errNoPubkey},
+		},
+		{
+			input: "enrtree-link=AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org",
+			err:   entryError{"link", errBadPubkey},
+		},
+		{
+			input: "enrtree-link=AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org",
+			err:   entryError{"link", errBadPubkey},
+		},
+		// ENRs
+		{
+			input: "enr=-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM=",
+			e:     &enrEntry{node: testNode(nodesSeed1)},
+		},
+		{
+			input: "enr=-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=",
+			err:   entryError{"enr", errInvalidENR},
+		},
+		// Invalid:
+		{input: "", err: errUnknownEntry},
+		{input: "foo", err: errUnknownEntry},
+		{input: "enrtree", err: errUnknownEntry},
+		{input: "enrtree-x=", err: errUnknownEntry},
+	}
+	for i, test := range tests {
+		e, err := parseEntry(test.input, enode.ValidSchemes)
+		if !reflect.DeepEqual(e, test.e) {
+			t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
+		}
+		if err != test.err {
+			t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
+		}
+	}
+}
+
+func TestMakeTree(t *testing.T) {
+	nodes := testNodes(nodesSeed2, 50)
+	tree, err := MakeTree(2, nodes, nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := tree.ToTXT("")
+	if len(txt) < len(nodes)+1 {
+		t.Fatal("too few TXT records in output")
+	}
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/CODE_OF_CONDUCT.md b/vendor/github.com/cloudflare/cloudflare-go/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..bfbc69d22
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/CODE_OF_CONDUCT.md
@@ -0,0 +1,77 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+  advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+  address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at ggalow@cloudflare.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
+
diff --git a/vendor/github.com/cloudflare/cloudflare-go/LICENSE b/vendor/github.com/cloudflare/cloudflare-go/LICENSE
new file mode 100644
index 000000000..33865c30f
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2015-2019, Cloudflare. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/cloudflare/cloudflare-go/README.md b/vendor/github.com/cloudflare/cloudflare-go/README.md
new file mode 100644
index 000000000..8f5d77b74
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/README.md
@@ -0,0 +1,107 @@
+# cloudflare-go
+
+[![GoDoc](https://img.shields.io/badge/godoc-reference-5673AF.svg?style=flat-square)](https://godoc.org/github.com/cloudflare/cloudflare-go)
+[![Build Status](https://img.shields.io/travis/cloudflare/cloudflare-go/master.svg?style=flat-square)](https://travis-ci.org/cloudflare/cloudflare-go)
+[![Go Report Card](https://goreportcard.com/badge/github.com/cloudflare/cloudflare-go?style=flat-square)](https://goreportcard.com/report/github.com/cloudflare/cloudflare-go)
+
+> **Note**: This library is under active development as we expand it to cover
+> our (expanding!) API. Consider the public API of this package a little
+> unstable as we work towards a v1.0.
+
+A Go library for interacting with
+[Cloudflare's API v4](https://api.cloudflare.com/). This library allows you to:
+
+* Manage and automate changes to your DNS records within Cloudflare
+* Manage and automate changes to your zones (domains) on Cloudflare, including
+  adding new zones to your account
+* List and modify the status of WAF (Web Application Firewall) rules for your
+  zones
+* Fetch Cloudflare's IP ranges for automating your firewall whitelisting
+
+A command-line client, [flarectl](cmd/flarectl), is also available as part of
+this project.
+
+## Features
+
+The current feature list includes:
+
+* [x] Cache purging
+* [x] Cloudflare IPs
+* [x] Custom hostnames
+* [x] DNS Records
+* [x] Firewall (partial)
+* [ ] [Keyless SSL](https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/)
+* [x] [Load Balancing](https://blog.cloudflare.com/introducing-load-balancing-intelligent-failover-with-cloudflare/)
+* [x] [Logpush Jobs](https://developers.cloudflare.com/logs/logpush/)
+* [ ] Organization Administration
+* [x] [Origin CA](https://blog.cloudflare.com/universal-ssl-encryption-all-the-way-to-the-origin-for-free/)
+* [x] [Railgun](https://www.cloudflare.com/railgun/) administration
+* [x] Rate Limiting
+* [x] User Administration (partial)
+* [x] Virtual DNS Management
+* [x] Web Application Firewall (WAF)
+* [x] Zone Lockdown and User-Agent Block rules
+* [x] Zones
+
+Pull Requests are welcome, but please open an issue (or comment in an existing
+issue) to discuss any non-trivial changes before submitting code.
+
+## Installation
+
+You need a working Go environment.
+
+```
+go get github.com/cloudflare/cloudflare-go
+```
+
+## Getting Started
+
+```go
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/cloudflare/cloudflare-go"
+)
+
+func main() {
+	// Construct a new API object
+	api, err := cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL"))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Fetch user details on the account
+	u, err := api.UserDetails()
+	if err != nil {
+		log.Fatal(err)
+	}
+	// Print user details
+	fmt.Println(u)
+
+	// Fetch the zone ID
+	id, err := api.ZoneIDByName("example.com") // Assuming example.com exists in your Cloudflare account already
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Fetch zone details
+	zone, err := api.ZoneDetails(id)
+	if err != nil {
+		log.Fatal(err)
+	}
+	// Print zone details
+	fmt.Println(zone)
+}
+```
+
+Also refer to the
+[API documentation](https://godoc.org/github.com/cloudflare/cloudflare-go) for
+how to use this package in-depth.
+
+# License
+
+BSD licensed. See the [LICENSE](LICENSE) file for details.
diff --git a/vendor/github.com/cloudflare/cloudflare-go/access_application.go b/vendor/github.com/cloudflare/cloudflare-go/access_application.go
new file mode 100644
index 000000000..0893c5681
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/access_application.go
@@ -0,0 +1,180 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessApplication represents an Access application.
+type AccessApplication struct {
+	ID              string     `json:"id,omitempty"`
+	CreatedAt       *time.Time `json:"created_at,omitempty"`
+	UpdatedAt       *time.Time `json:"updated_at,omitempty"`
+	AUD             string     `json:"aud,omitempty"`
+	Name            string     `json:"name"`
+	Domain          string     `json:"domain"`
+	SessionDuration string     `json:"session_duration,omitempty"`
+}
+
+// AccessApplicationListResponse represents the response from the list
+// access applications endpoint.
+type AccessApplicationListResponse struct {
+	Result []AccessApplication `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccessApplicationDetailResponse is the API response, containing a single
+// access application.
+type AccessApplicationDetailResponse struct {
+	Success  bool              `json:"success"`
+	Errors   []string          `json:"errors"`
+	Messages []string          `json:"messages"`
+	Result   AccessApplication `json:"result"`
+}
+
+// AccessApplications returns all applications within a zone.
+//
+// API reference: https://api.cloudflare.com/#access-applications-list-access-applications
+func (api *API) AccessApplications(zoneID string, pageOpts PaginationOptions) ([]AccessApplication, ResultInfo, error) {
+	v := url.Values{}
+	if pageOpts.PerPage > 0 {
+		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
+	}
+	if pageOpts.Page > 0 {
+		v.Set("page", strconv.Itoa(pageOpts.Page))
+	}
+
+	uri := "/zones/" + zoneID + "/access/apps"
+	if len(v) > 0 {
+		uri = uri + "?" + v.Encode()
+	}
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []AccessApplication{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessApplicationListResponse AccessApplicationListResponse
+	err = json.Unmarshal(res, &accessApplicationListResponse)
+	if err != nil {
+		return []AccessApplication{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessApplicationListResponse.Result, accessApplicationListResponse.ResultInfo, nil
+}
+
+// AccessApplication returns a single application based on the
+// application ID.
+//
+// API reference: https://api.cloudflare.com/#access-applications-access-applications-details
+func (api *API) AccessApplication(zoneID, applicationID string) (AccessApplication, error) {
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s",
+		zoneID,
+		applicationID,
+	)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AccessApplication{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessApplicationDetailResponse AccessApplicationDetailResponse
+	err = json.Unmarshal(res, &accessApplicationDetailResponse)
+	if err != nil {
+		return AccessApplication{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessApplicationDetailResponse.Result, nil
+}
+
+// CreateAccessApplication creates a new access application.
+//
+// API reference: https://api.cloudflare.com/#access-applications-create-access-application
+func (api *API) CreateAccessApplication(zoneID string, accessApplication AccessApplication) (AccessApplication, error) {
+	uri := "/zones/" + zoneID + "/access/apps"
+
+	res, err := api.makeRequest("POST", uri, accessApplication)
+	if err != nil {
+		return AccessApplication{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessApplicationDetailResponse AccessApplicationDetailResponse
+	err = json.Unmarshal(res, &accessApplicationDetailResponse)
+	if err != nil {
+		return AccessApplication{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessApplicationDetailResponse.Result, nil
+}
+
+// UpdateAccessApplication updates an existing access application.
+//
+// API reference: https://api.cloudflare.com/#access-applications-update-access-application
+func (api *API) UpdateAccessApplication(zoneID string, accessApplication AccessApplication) (AccessApplication, error) {
+	if accessApplication.ID == "" {
+		return AccessApplication{}, errors.Errorf("access application ID cannot be empty")
+	}
+
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s",
+		zoneID,
+		accessApplication.ID,
+	)
+
+	res, err := api.makeRequest("PUT", uri, accessApplication)
+	if err != nil {
+		return AccessApplication{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessApplicationDetailResponse AccessApplicationDetailResponse
+	err = json.Unmarshal(res, &accessApplicationDetailResponse)
+	if err != nil {
+		return AccessApplication{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessApplicationDetailResponse.Result, nil
+}
+
+// DeleteAccessApplication deletes an access application.
+//
+// API reference: https://api.cloudflare.com/#access-applications-delete-access-application
+func (api *API) DeleteAccessApplication(zoneID, applicationID string) error {
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s",
+		zoneID,
+		applicationID,
+	)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
+
+// RevokeAccessApplicationTokens revokes tokens associated with an
+// access application.
+//
+// API reference: https://api.cloudflare.com/#access-applications-revoke-access-tokens
+func (api *API) RevokeAccessApplicationTokens(zoneID, applicationID string) error {
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s/revoke-tokens",
+		zoneID,
+		applicationID,
+	)
+
+	_, err := api.makeRequest("POST", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/access_identity_provider.go b/vendor/github.com/cloudflare/cloudflare-go/access_identity_provider.go
new file mode 100644
index 000000000..b41ed8ff0
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/access_identity_provider.go
@@ -0,0 +1,331 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/pkg/errors"
+)
+
+// AccessIdentityProvider is the structure of the provider object.
+type AccessIdentityProvider struct {
+	ID     string      `json:"id,omitemtpy"`
+	Name   string      `json:"name"`
+	Type   string      `json:"type"`
+	Config interface{} `json:"config"`
+}
+
+// AccessAzureADConfiguration is the representation of the Azure AD identity
+// provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/azuread/
+type AccessAzureADConfiguration struct {
+	ClientID      string `json:"client_id"`
+	ClientSecret  string `json:"client_secret"`
+	DirectoryID   string `json:"directory_id"`
+	SupportGroups bool   `json:"support_groups"`
+}
+
+// AccessCentrifyConfiguration is the representation of the Centrify identity
+// provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/centrify/
+type AccessCentrifyConfiguration struct {
+	ClientID        string `json:"client_id"`
+	ClientSecret    string `json:"client_secret"`
+	CentrifyAccount string `json:"centrify_account"`
+	CentrifyAppID   string `json:"centrify_app_id"`
+}
+
+// AccessCentrifySAMLConfiguration is the representation of the Centrify
+// identity provider using SAML.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/saml-centrify/
+type AccessCentrifySAMLConfiguration struct {
+	IssuerURL          string   `json:"issuer_url"`
+	SsoTargetURL       string   `json:"sso_target_url"`
+	Attributes         []string `json:"attributes"`
+	EmailAttributeName string   `json:"email_attribute_name"`
+	SignRequest        bool     `json:"sign_request"`
+	IdpPublicCert      string   `json:"idp_public_cert"`
+}
+
+// AccessFacebookConfiguration is the representation of the Facebook identity
+// provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/facebook-login/
+type AccessFacebookConfiguration struct {
+	ClientID     string `json:"client_id"`
+	ClientSecret string `json:"client_secret"`
+}
+
+// AccessGSuiteConfiguration is the representation of the GSuite identity
+// provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/gsuite/
+type AccessGSuiteConfiguration struct {
+	ClientID     string `json:"client_id"`
+	ClientSecret string `json:"client_secret"`
+	AppsDomain   string `json:"apps_domain"`
+}
+
+// AccessGenericOIDCConfiguration is the representation of the generic OpenID
+// Connect (OIDC) connector.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/generic-oidc/
+type AccessGenericOIDCConfiguration struct {
+	ClientID     string `json:"client_id"`
+	ClientSecret string `json:"client_secret"`
+	AuthURL      string `json:"auth_url"`
+	TokenURL     string `json:"token_url"`
+	CertsURL     string `json:"certs_url"`
+}
+
+// AccessGitHubConfiguration is the representation of the GitHub identity
+// provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/github/
+type AccessGitHubConfiguration struct {
+	ClientID     string `json:"client_id"`
+	ClientSecret string `json:"client_secret"`
+}
+
+// AccessGoogleConfiguration is the representation of the Google identity
+// provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/google/
+type AccessGoogleConfiguration struct {
+	ClientID     string `json:"client_id"`
+	ClientSecret string `json:"client_secret"`
+}
+
+// AccessJumpCloudSAMLConfiguration is the representation of the Jump Cloud
+// identity provider using SAML.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/jumpcloud-saml/
+type AccessJumpCloudSAMLConfiguration struct {
+	IssuerURL          string   `json:"issuer_url"`
+	SsoTargetURL       string   `json:"sso_target_url"`
+	Attributes         []string `json:"attributes"`
+	EmailAttributeName string   `json:"email_attribute_name"`
+	SignRequest        bool     `json:"sign_request"`
+	IdpPublicCert      string   `json:"idp_public_cert"`
+}
+
+// AccessOktaConfiguration is the representation of the Okta identity provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/okta/
+type AccessOktaConfiguration struct {
+	ClientID     string `json:"client_id"`
+	ClientSecret string `json:"client_secret"`
+	OktaAccount  string `json:"okta_account"`
+}
+
+// AccessOktaSAMLConfiguration is the representation of the Okta identity
+// provider using SAML.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/saml-okta/
+type AccessOktaSAMLConfiguration struct {
+	IssuerURL          string   `json:"issuer_url"`
+	SsoTargetURL       string   `json:"sso_target_url"`
+	Attributes         []string `json:"attributes"`
+	EmailAttributeName string   `json:"email_attribute_name"`
+	SignRequest        bool     `json:"sign_request"`
+	IdpPublicCert      string   `json:"idp_public_cert"`
+}
+
+// AccessOneTimePinConfiguration is the representation of the default One Time
+// Pin identity provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/one-time-pin/
+type AccessOneTimePinConfiguration struct{}
+
+// AccessOneLoginOIDCConfiguration is the representation of the OneLogin
+// OpenID connector as an identity provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/onelogin-oidc/
+type AccessOneLoginOIDCConfiguration struct {
+	ClientID        string `json:"client_id"`
+	ClientSecret    string `json:"client_secret"`
+	OneloginAccount string `json:"onelogin_account"`
+}
+
+// AccessOneLoginSAMLConfiguration is the representation of the OneLogin
+// identity provider using SAML.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/onelogin-saml/
+type AccessOneLoginSAMLConfiguration struct {
+	IssuerURL          string   `json:"issuer_url"`
+	SsoTargetURL       string   `json:"sso_target_url"`
+	Attributes         []string `json:"attributes"`
+	EmailAttributeName string   `json:"email_attribute_name"`
+	SignRequest        bool     `json:"sign_request"`
+	IdpPublicCert      string   `json:"idp_public_cert"`
+}
+
+// AccessPingSAMLConfiguration is the representation of the Ping identity
+// provider using SAML.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/ping-saml/
+type AccessPingSAMLConfiguration struct {
+	IssuerURL          string   `json:"issuer_url"`
+	SsoTargetURL       string   `json:"sso_target_url"`
+	Attributes         []string `json:"attributes"`
+	EmailAttributeName string   `json:"email_attribute_name"`
+	SignRequest        bool     `json:"sign_request"`
+	IdpPublicCert      string   `json:"idp_public_cert"`
+}
+
+// AccessYandexConfiguration is the representation of the Yandex identity provider.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/yandex/
+type AccessYandexConfiguration struct {
+	ClientID     string `json:"client_id"`
+	ClientSecret string `json:"client_secret"`
+}
+
+// AccessADSAMLConfiguration is the representation of the Active Directory
+// identity provider using SAML.
+//
+// API reference: https://developers.cloudflare.com/access/configuring-identity-providers/adfs/
+type AccessADSAMLConfiguration struct {
+	IssuerURL          string   `json:"issuer_url"`
+	SsoTargetURL       string   `json:"sso_target_url"`
+	Attributes         []string `json:"attributes"`
+	EmailAttributeName string   `json:"email_attribute_name"`
+	SignRequest        bool     `json:"sign_request"`
+	IdpPublicCert      string   `json:"idp_public_cert"`
+}
+
+// AccessIdentityProvidersListResponse is the API response for multiple
+// Access Identity Providers.
+type AccessIdentityProvidersListResponse struct {
+	Success  bool                     `json:"success"`
+	Errors   []string                 `json:"errors"`
+	Messages []string                 `json:"messages"`
+	Result   []AccessIdentityProvider `json:"result"`
+}
+
+// AccessIdentityProviderListResponse is the API response for a single
+// Access Identity Provider.
+type AccessIdentityProviderListResponse struct {
+	Success  bool                   `json:"success"`
+	Errors   []string               `json:"errors"`
+	Messages []string               `json:"messages"`
+	Result   AccessIdentityProvider `json:"result"`
+}
+
+// AccessIdentityProviders returns all Access Identity Providers for an
+// account.
+//
+// API reference: https://api.cloudflare.com/#access-identity-providers-list-access-identity-providers
+func (api *API) AccessIdentityProviders(accountID string) ([]AccessIdentityProvider, error) {
+	uri := "/accounts/" + accountID + "/access/identity_providers"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []AccessIdentityProvider{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessIdentityProviderResponse AccessIdentityProvidersListResponse
+	err = json.Unmarshal(res, &accessIdentityProviderResponse)
+	if err != nil {
+		return []AccessIdentityProvider{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessIdentityProviderResponse.Result, nil
+}
+
+// AccessIdentityProviderDetails returns a single Access Identity
+// Provider for an account.
+//
+// API reference: https://api.cloudflare.com/#access-identity-providers-access-identity-providers-details
+func (api *API) AccessIdentityProviderDetails(accountID, identityProviderID string) (AccessIdentityProvider, error) {
+	uri := fmt.Sprintf(
+		"/accounts/%s/access/identity_providers/%s",
+		accountID,
+		identityProviderID,
+	)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessIdentityProviderResponse AccessIdentityProviderListResponse
+	err = json.Unmarshal(res, &accessIdentityProviderResponse)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessIdentityProviderResponse.Result, nil
+}
+
+// CreateAccessIdentityProvider creates a new Access Identity Provider.
+//
+// API reference: https://api.cloudflare.com/#access-identity-providers-create-access-identity-provider
+func (api *API) CreateAccessIdentityProvider(accountID string, identityProviderConfiguration AccessIdentityProvider) (AccessIdentityProvider, error) {
+	uri := "/accounts/" + accountID + "/access/identity_providers"
+
+	res, err := api.makeRequest("POST", uri, identityProviderConfiguration)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessIdentityProviderResponse AccessIdentityProviderListResponse
+	err = json.Unmarshal(res, &accessIdentityProviderResponse)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessIdentityProviderResponse.Result, nil
+}
+
+// UpdateAccessIdentityProvider updates an existing Access Identity
+// Provider.
+//
+// API reference: https://api.cloudflare.com/#access-identity-providers-create-access-identity-provider
+func (api *API) UpdateAccessIdentityProvider(accountID, identityProviderUUID string, identityProviderConfiguration AccessIdentityProvider) (AccessIdentityProvider, error) {
+	uri := fmt.Sprintf(
+		"/accounts/%s/access/identity_providers/%s",
+		accountID,
+		identityProviderUUID,
+	)
+
+	res, err := api.makeRequest("PUT", uri, identityProviderConfiguration)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessIdentityProviderResponse AccessIdentityProviderListResponse
+	err = json.Unmarshal(res, &accessIdentityProviderResponse)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessIdentityProviderResponse.Result, nil
+}
+
+// DeleteAccessIdentityProvider deletes an Access Identity Provider.
+//
+// API reference: https://api.cloudflare.com/#access-identity-providers-create-access-identity-provider
+func (api *API) DeleteAccessIdentityProvider(accountID, identityProviderUUID string) (AccessIdentityProvider, error) {
+	uri := fmt.Sprintf(
+		"/accounts/%s/access/identity_providers/%s",
+		accountID,
+		identityProviderUUID,
+	)
+
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessIdentityProviderResponse AccessIdentityProviderListResponse
+	err = json.Unmarshal(res, &accessIdentityProviderResponse)
+	if err != nil {
+		return AccessIdentityProvider{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessIdentityProviderResponse.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/access_organization.go b/vendor/github.com/cloudflare/cloudflare-go/access_organization.go
new file mode 100644
index 000000000..5bc4a16aa
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/access_organization.go
@@ -0,0 +1,101 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessOrganization represents an Access organization.
+type AccessOrganization struct {
+	CreatedAt   *time.Time                    `json:"created_at"`
+	UpdatedAt   *time.Time                    `json:"updated_at"`
+	Name        string                        `json:"name"`
+	AuthDomain  string                        `json:"auth_domain"`
+	LoginDesign AccessOrganizationLoginDesign `json:"login_design"`
+}
+
+// AccessOrganizationLoginDesign represents the login design options.
+type AccessOrganizationLoginDesign struct {
+	BackgroundColor string `json:"background_color"`
+	TextColor       string `json:"text_color"`
+	LogoPath        string `json:"logo_path"`
+}
+
+// AccessOrganizationListResponse represents the response from the list
+// access organization endpoint.
+type AccessOrganizationListResponse struct {
+	Result AccessOrganization `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccessOrganizationDetailResponse is the API response, containing a
+// single access organization.
+type AccessOrganizationDetailResponse struct {
+	Success  bool               `json:"success"`
+	Errors   []string           `json:"errors"`
+	Messages []string           `json:"messages"`
+	Result   AccessOrganization `json:"result"`
+}
+
+// AccessOrganization returns the Access organisation details.
+//
+// API reference: https://api.cloudflare.com/#access-organizations-access-organization-details
+func (api *API) AccessOrganization(accountID string) (AccessOrganization, ResultInfo, error) {
+	uri := "/accounts/" + accountID + "/access/organizations"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AccessOrganization{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessOrganizationListResponse AccessOrganizationListResponse
+	err = json.Unmarshal(res, &accessOrganizationListResponse)
+	if err != nil {
+		return AccessOrganization{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessOrganizationListResponse.Result, accessOrganizationListResponse.ResultInfo, nil
+}
+
+// CreateAccessOrganization creates the Access organisation details.
+//
+// API reference: https://api.cloudflare.com/#access-organizations-create-access-organization
+func (api *API) CreateAccessOrganization(accountID string, accessOrganization AccessOrganization) (AccessOrganization, error) {
+	uri := "/accounts/" + accountID + "/access/organizations"
+
+	res, err := api.makeRequest("POST", uri, accessOrganization)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessOrganizationDetailResponse AccessOrganizationDetailResponse
+	err = json.Unmarshal(res, &accessOrganizationDetailResponse)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessOrganizationDetailResponse.Result, nil
+}
+
+// UpdateAccessOrganization creates the Access organisation details.
+//
+// API reference: https://api.cloudflare.com/#access-organizations-update-access-organization
+func (api *API) UpdateAccessOrganization(accountID string, accessOrganization AccessOrganization) (AccessOrganization, error) {
+	uri := "/accounts/" + accountID + "/access/organizations"
+
+	res, err := api.makeRequest("PUT", uri, accessOrganization)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessOrganizationDetailResponse AccessOrganizationDetailResponse
+	err = json.Unmarshal(res, &accessOrganizationDetailResponse)
+	if err != nil {
+		return AccessOrganization{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessOrganizationDetailResponse.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/access_policy.go b/vendor/github.com/cloudflare/cloudflare-go/access_policy.go
new file mode 100644
index 000000000..dbf63e49f
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/access_policy.go
@@ -0,0 +1,221 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessPolicy defines a policy for allowing or disallowing access to
+// one or more Access applications.
+type AccessPolicy struct {
+	ID         string     `json:"id,omitempty"`
+	Precedence int        `json:"precedence"`
+	Decision   string     `json:"decision"`
+	CreatedAt  *time.Time `json:"created_at"`
+	UpdatedAt  *time.Time `json:"updated_at"`
+	Name       string     `json:"name"`
+
+	// The include policy works like an OR logical operator. The user must
+	// satisfy one of the rules.
+	Include []interface{} `json:"include"`
+
+	// The exclude policy works like a NOT logical operator. The user must
+	// not satisfy all of the rules in exclude.
+	Exclude []interface{} `json:"exclude"`
+
+	// The require policy works like a AND logical operator. The user must
+	// satisfy all of the rules in require.
+	Require []interface{} `json:"require"`
+}
+
+// AccessPolicyEmail is used for managing access based on the email.
+// For example, restrict access to users with the email addresses
+// `test@example.com` or `someone@example.com`.
+type AccessPolicyEmail struct {
+	Email struct {
+		Email string `json:"email"`
+	} `json:"email"`
+}
+
+// AccessPolicyEmailDomain is used for managing access based on an email
+// domain domain such as `example.com` instead of individual addresses.
+type AccessPolicyEmailDomain struct {
+	EmailDomain struct {
+		Domain string `json:"domain"`
+	} `json:"email_domain"`
+}
+
+// AccessPolicyIP is used for managing access based in the IP. It
+// accepts individual IPs or CIDRs.
+type AccessPolicyIP struct {
+	IP struct {
+		IP string `json:"ip"`
+	} `json:"ip"`
+}
+
+// AccessPolicyEveryone is used for managing access to everyone.
+type AccessPolicyEveryone struct {
+	Everyone struct{} `json:"everyone"`
+}
+
+// AccessPolicyAccessGroup is used for managing access based on an
+// access group.
+type AccessPolicyAccessGroup struct {
+	Group struct {
+		ID string `json:"id"`
+	} `json:"group"`
+}
+
+// AccessPolicyListResponse represents the response from the list
+// access polciies endpoint.
+type AccessPolicyListResponse struct {
+	Result []AccessPolicy `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccessPolicyDetailResponse is the API response, containing a single
+// access policy.
+type AccessPolicyDetailResponse struct {
+	Success  bool         `json:"success"`
+	Errors   []string     `json:"errors"`
+	Messages []string     `json:"messages"`
+	Result   AccessPolicy `json:"result"`
+}
+
+// AccessPolicies returns all access policies for an access application.
+//
+// API reference: https://api.cloudflare.com/#access-policy-list-access-policies
+func (api *API) AccessPolicies(zoneID, applicationID string, pageOpts PaginationOptions) ([]AccessPolicy, ResultInfo, error) {
+	v := url.Values{}
+	if pageOpts.PerPage > 0 {
+		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
+	}
+	if pageOpts.Page > 0 {
+		v.Set("page", strconv.Itoa(pageOpts.Page))
+	}
+
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s/policies",
+		zoneID,
+		applicationID,
+	)
+
+	if len(v) > 0 {
+		uri = uri + "?" + v.Encode()
+	}
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []AccessPolicy{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessPolicyListResponse AccessPolicyListResponse
+	err = json.Unmarshal(res, &accessPolicyListResponse)
+	if err != nil {
+		return []AccessPolicy{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessPolicyListResponse.Result, accessPolicyListResponse.ResultInfo, nil
+}
+
+// AccessPolicy returns a single policy based on the policy ID.
+//
+// API reference: https://api.cloudflare.com/#access-policy-access-policy-details
+func (api *API) AccessPolicy(zoneID, applicationID, policyID string) (AccessPolicy, error) {
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s/policies/%s",
+		zoneID,
+		applicationID,
+		policyID,
+	)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AccessPolicy{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessPolicyDetailResponse AccessPolicyDetailResponse
+	err = json.Unmarshal(res, &accessPolicyDetailResponse)
+	if err != nil {
+		return AccessPolicy{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessPolicyDetailResponse.Result, nil
+}
+
+// CreateAccessPolicy creates a new access policy.
+//
+// API reference: https://api.cloudflare.com/#access-policy-create-access-policy
+func (api *API) CreateAccessPolicy(zoneID, applicationID string, accessPolicy AccessPolicy) (AccessPolicy, error) {
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s/policies",
+		zoneID,
+		applicationID,
+	)
+
+	res, err := api.makeRequest("POST", uri, accessPolicy)
+	if err != nil {
+		return AccessPolicy{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessPolicyDetailResponse AccessPolicyDetailResponse
+	err = json.Unmarshal(res, &accessPolicyDetailResponse)
+	if err != nil {
+		return AccessPolicy{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessPolicyDetailResponse.Result, nil
+}
+
+// UpdateAccessPolicy updates an existing access policy.
+//
+// API reference: https://api.cloudflare.com/#access-policy-update-access-policy
+func (api *API) UpdateAccessPolicy(zoneID, applicationID string, accessPolicy AccessPolicy) (AccessPolicy, error) {
+	if accessPolicy.ID == "" {
+		return AccessPolicy{}, errors.Errorf("access policy ID cannot be empty")
+	}
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s/policies/%s",
+		zoneID,
+		applicationID,
+		accessPolicy.ID,
+	)
+
+	res, err := api.makeRequest("PUT", uri, accessPolicy)
+	if err != nil {
+		return AccessPolicy{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessPolicyDetailResponse AccessPolicyDetailResponse
+	err = json.Unmarshal(res, &accessPolicyDetailResponse)
+	if err != nil {
+		return AccessPolicy{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessPolicyDetailResponse.Result, nil
+}
+
+// DeleteAccessPolicy deletes an access policy.
+//
+// API reference: https://api.cloudflare.com/#access-policy-update-access-policy
+func (api *API) DeleteAccessPolicy(zoneID, applicationID, accessPolicyID string) error {
+	uri := fmt.Sprintf(
+		"/zones/%s/access/apps/%s/policies/%s",
+		zoneID,
+		applicationID,
+		accessPolicyID,
+	)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/access_service_tokens.go b/vendor/github.com/cloudflare/cloudflare-go/access_service_tokens.go
new file mode 100644
index 000000000..66a2bb794
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/access_service_tokens.go
@@ -0,0 +1,167 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessServiceToken represents an Access Service Token.
+type AccessServiceToken struct {
+	ClientID  string     `json:"client_id"`
+	CreatedAt *time.Time `json:"created_at"`
+	ExpiresAt *time.Time `json:"expires_at"`
+	ID        string     `json:"id"`
+	Name      string     `json:"name"`
+	UpdatedAt *time.Time `json:"updated_at"`
+}
+
+// AccessServiceTokenUpdateResponse represents the response from the API
+// when a new Service Token is updated. This base struct is also used in the
+// Create as they are very similar responses.
+type AccessServiceTokenUpdateResponse struct {
+	CreatedAt *time.Time `json:"created_at"`
+	UpdatedAt *time.Time `json:"updated_at"`
+	ID        string     `json:"id"`
+	Name      string     `json:"name"`
+	ClientID  string     `json:"client_id"`
+}
+
+// AccessServiceTokenCreateResponse is the same API response as the Update
+// operation with the exception that the `ClientSecret` is present in a
+// Create operation.
+type AccessServiceTokenCreateResponse struct {
+	CreatedAt    *time.Time `json:"created_at"`
+	UpdatedAt    *time.Time `json:"updated_at"`
+	ID           string     `json:"id"`
+	Name         string     `json:"name"`
+	ClientID     string     `json:"client_id"`
+	ClientSecret string     `json:"client_secret"`
+}
+
+// AccessServiceTokensListResponse represents the response from the list
+// Access Service Tokens endpoint.
+type AccessServiceTokensListResponse struct {
+	Result []AccessServiceToken `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccessServiceTokensDetailResponse is the API response, containing a single
+// Access Service Token.
+type AccessServiceTokensDetailResponse struct {
+	Success  bool               `json:"success"`
+	Errors   []string           `json:"errors"`
+	Messages []string           `json:"messages"`
+	Result   AccessServiceToken `json:"result"`
+}
+
+// AccessServiceTokensCreationDetailResponse is the API response, containing a
+// single Access Service Token.
+type AccessServiceTokensCreationDetailResponse struct {
+	Success  bool                             `json:"success"`
+	Errors   []string                         `json:"errors"`
+	Messages []string                         `json:"messages"`
+	Result   AccessServiceTokenCreateResponse `json:"result"`
+}
+
+// AccessServiceTokensUpdateDetailResponse is the API response, containing a
+// single Access Service Token.
+type AccessServiceTokensUpdateDetailResponse struct {
+	Success  bool                             `json:"success"`
+	Errors   []string                         `json:"errors"`
+	Messages []string                         `json:"messages"`
+	Result   AccessServiceTokenUpdateResponse `json:"result"`
+}
+
+// AccessServiceTokens returns all Access Service Tokens for an account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-list-access-service-tokens
+func (api *API) AccessServiceTokens(accountID string) ([]AccessServiceToken, ResultInfo, error) {
+	uri := "/accounts/" + accountID + "/access/service_tokens"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []AccessServiceToken{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokensListResponse AccessServiceTokensListResponse
+	err = json.Unmarshal(res, &accessServiceTokensListResponse)
+	if err != nil {
+		return []AccessServiceToken{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokensListResponse.Result, accessServiceTokensListResponse.ResultInfo, nil
+}
+
+// CreateAccessServiceToken creates a new Access Service Token for an account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-create-access-service-token
+func (api *API) CreateAccessServiceToken(accountID, name string) (AccessServiceTokenCreateResponse, error) {
+	uri := "/accounts/" + accountID + "/access/service_tokens"
+	marshalledName, _ := json.Marshal(struct {
+		Name string `json:"name"`
+	}{name})
+
+	res, err := api.makeRequest("POST", uri, marshalledName)
+
+	if err != nil {
+		return AccessServiceTokenCreateResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokenCreation AccessServiceTokensCreationDetailResponse
+	err = json.Unmarshal(res, &accessServiceTokenCreation)
+	if err != nil {
+		return AccessServiceTokenCreateResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokenCreation.Result, nil
+}
+
+// UpdateAccessServiceToken updates an existing Access Service Token for an
+// account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-update-access-service-token
+func (api *API) UpdateAccessServiceToken(accountID, uuid, name string) (AccessServiceTokenUpdateResponse, error) {
+	uri := fmt.Sprintf("/accounts/%s/access/service_tokens/%s", accountID, uuid)
+
+	marshalledName, _ := json.Marshal(struct {
+		Name string `json:"name"`
+	}{name})
+
+	res, err := api.makeRequest("PUT", uri, marshalledName)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokenUpdate AccessServiceTokensUpdateDetailResponse
+	err = json.Unmarshal(res, &accessServiceTokenUpdate)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokenUpdate.Result, nil
+}
+
+// DeleteAccessServiceToken removes an existing Access Service Token for an
+// account.
+//
+// API reference: https://api.cloudflare.com/#access-service-tokens-delete-access-service-token
+func (api *API) DeleteAccessServiceToken(accountID, uuid string) (AccessServiceTokenUpdateResponse, error) {
+	uri := fmt.Sprintf("/accounts/%s/access/service_tokens/%s", accountID, uuid)
+
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessServiceTokenUpdate AccessServiceTokensUpdateDetailResponse
+	err = json.Unmarshal(res, &accessServiceTokenUpdate)
+	if err != nil {
+		return AccessServiceTokenUpdateResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessServiceTokenUpdate.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/account_members.go b/vendor/github.com/cloudflare/cloudflare-go/account_members.go
new file mode 100644
index 000000000..42166e922
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/account_members.go
@@ -0,0 +1,186 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+// AccountMember is the definition of a member of an account.
+type AccountMember struct {
+	ID     string                   `json:"id"`
+	Code   string                   `json:"code"`
+	User   AccountMemberUserDetails `json:"user"`
+	Status string                   `json:"status"`
+	Roles  []AccountRole            `json:"roles"`
+}
+
+// AccountMemberUserDetails outlines all the personal information about
+// a member.
+type AccountMemberUserDetails struct {
+	ID                             string `json:"id"`
+	FirstName                      string `json:"first_name"`
+	LastName                       string `json:"last_name"`
+	Email                          string `json:"email"`
+	TwoFactorAuthenticationEnabled bool
+}
+
+// AccountMembersListResponse represents the response from the list
+// account members endpoint.
+type AccountMembersListResponse struct {
+	Result []AccountMember `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccountMemberDetailResponse is the API response, containing a single
+// account member.
+type AccountMemberDetailResponse struct {
+	Success  bool          `json:"success"`
+	Errors   []string      `json:"errors"`
+	Messages []string      `json:"messages"`
+	Result   AccountMember `json:"result"`
+}
+
+// AccountMemberInvitation represents the invitation for a new member to
+// the account.
+type AccountMemberInvitation struct {
+	Email string   `json:"email"`
+	Roles []string `json:"roles"`
+}
+
+// AccountMembers returns all members of an account.
+//
+// API reference: https://api.cloudflare.com/#accounts-list-accounts
+func (api *API) AccountMembers(accountID string, pageOpts PaginationOptions) ([]AccountMember, ResultInfo, error) {
+	if accountID == "" {
+		return []AccountMember{}, ResultInfo{}, errors.New(errMissingAccountID)
+	}
+
+	v := url.Values{}
+	if pageOpts.PerPage > 0 {
+		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
+	}
+	if pageOpts.Page > 0 {
+		v.Set("page", strconv.Itoa(pageOpts.Page))
+	}
+
+	uri := "/accounts/" + accountID + "/members"
+	if len(v) > 0 {
+		uri = uri + "?" + v.Encode()
+	}
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []AccountMember{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accountMemberListresponse AccountMembersListResponse
+	err = json.Unmarshal(res, &accountMemberListresponse)
+	if err != nil {
+		return []AccountMember{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accountMemberListresponse.Result, accountMemberListresponse.ResultInfo, nil
+}
+
+// CreateAccountMember invites a new member to join an account.
+//
+// API reference: https://api.cloudflare.com/#account-members-add-member
+func (api *API) CreateAccountMember(accountID string, emailAddress string, roles []string) (AccountMember, error) {
+	if accountID == "" {
+		return AccountMember{}, errors.New(errMissingAccountID)
+	}
+
+	uri := "/accounts/" + accountID + "/members"
+
+	var newMember = AccountMemberInvitation{
+		Email: emailAddress,
+		Roles: roles,
+	}
+	res, err := api.makeRequest("POST", uri, newMember)
+	if err != nil {
+		return AccountMember{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accountMemberListResponse AccountMemberDetailResponse
+	err = json.Unmarshal(res, &accountMemberListResponse)
+	if err != nil {
+		return AccountMember{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accountMemberListResponse.Result, nil
+}
+
+// DeleteAccountMember removes a member from an account.
+//
+// API reference: https://api.cloudflare.com/#account-members-remove-member
+func (api *API) DeleteAccountMember(accountID string, userID string) error {
+	if accountID == "" {
+		return errors.New(errMissingAccountID)
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/members/%s", accountID, userID)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
+
+// UpdateAccountMember modifies an existing account member.
+//
+// API reference: https://api.cloudflare.com/#account-members-update-member
+func (api *API) UpdateAccountMember(accountID string, userID string, member AccountMember) (AccountMember, error) {
+	if accountID == "" {
+		return AccountMember{}, errors.New(errMissingAccountID)
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/members/%s", accountID, userID)
+
+	res, err := api.makeRequest("PUT", uri, member)
+	if err != nil {
+		return AccountMember{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accountMemberListResponse AccountMemberDetailResponse
+	err = json.Unmarshal(res, &accountMemberListResponse)
+	if err != nil {
+		return AccountMember{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accountMemberListResponse.Result, nil
+}
+
+// AccountMember returns details of a single account member.
+//
+// API reference: https://api.cloudflare.com/#account-members-member-details
+func (api *API) AccountMember(accountID string, memberID string) (AccountMember, error) {
+	if accountID == "" {
+		return AccountMember{}, errors.New(errMissingAccountID)
+	}
+
+	uri := fmt.Sprintf(
+		"/accounts/%s/members/%s",
+		accountID,
+		memberID,
+	)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AccountMember{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accountMemberResponse AccountMemberDetailResponse
+	err = json.Unmarshal(res, &accountMemberResponse)
+	if err != nil {
+		return AccountMember{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accountMemberResponse.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/account_roles.go b/vendor/github.com/cloudflare/cloudflare-go/account_roles.go
new file mode 100644
index 000000000..3704313b5
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/account_roles.go
@@ -0,0 +1,80 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/pkg/errors"
+)
+
+// AccountRole defines the roles that a member can have attached.
+type AccountRole struct {
+	ID          string                           `json:"id"`
+	Name        string                           `json:"name"`
+	Description string                           `json:"description"`
+	Permissions map[string]AccountRolePermission `json:"permissions"`
+}
+
+// AccountRolePermission is the shared structure for all permissions
+// that can be assigned to a member.
+type AccountRolePermission struct {
+	Read bool `json:"read"`
+	Edit bool `json:"edit"`
+}
+
+// AccountRolesListResponse represents the list response from the
+// account roles.
+type AccountRolesListResponse struct {
+	Result []AccountRole `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccountRoleDetailResponse is the API response, containing a single
+// account role.
+type AccountRoleDetailResponse struct {
+	Success  bool        `json:"success"`
+	Errors   []string    `json:"errors"`
+	Messages []string    `json:"messages"`
+	Result   AccountRole `json:"result"`
+}
+
+// AccountRoles returns all roles of an account.
+//
+// API reference: https://api.cloudflare.com/#account-roles-list-roles
+func (api *API) AccountRoles(accountID string) ([]AccountRole, error) {
+	uri := "/accounts/" + accountID + "/roles"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []AccountRole{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accountRolesListResponse AccountRolesListResponse
+	err = json.Unmarshal(res, &accountRolesListResponse)
+	if err != nil {
+		return []AccountRole{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accountRolesListResponse.Result, nil
+}
+
+// AccountRole returns the details of a single account role.
+//
+// API reference: https://api.cloudflare.com/#account-roles-role-details
+func (api *API) AccountRole(accountID string, roleID string) (AccountRole, error) {
+	uri := fmt.Sprintf("/accounts/%s/roles/%s", accountID, roleID)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AccountRole{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accountRole AccountRoleDetailResponse
+	err = json.Unmarshal(res, &accountRole)
+	if err != nil {
+		return AccountRole{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accountRole.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/accounts.go b/vendor/github.com/cloudflare/cloudflare-go/accounts.go
new file mode 100644
index 000000000..7d34b7b8f
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/accounts.go
@@ -0,0 +1,114 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+// AccountSettings outlines the available options for an account.
+type AccountSettings struct {
+	EnforceTwoFactor bool `json:"enforce_twofactor"`
+}
+
+// Account represents the root object that owns resources.
+type Account struct {
+	ID       string           `json:"id,omitempty"`
+	Name     string           `json:"name,omitempty"`
+	Settings *AccountSettings `json:"settings"`
+}
+
+// AccountResponse represents the response from the accounts endpoint for a
+// single account ID.
+type AccountResponse struct {
+	Result Account `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccountListResponse represents the response from the list accounts endpoint.
+type AccountListResponse struct {
+	Result []Account `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccountDetailResponse is the API response, containing a single Account.
+type AccountDetailResponse struct {
+	Success  bool     `json:"success"`
+	Errors   []string `json:"errors"`
+	Messages []string `json:"messages"`
+	Result   Account  `json:"result"`
+}
+
+// Accounts returns all accounts the logged in user has access to.
+//
+// API reference: https://api.cloudflare.com/#accounts-list-accounts
+func (api *API) Accounts(pageOpts PaginationOptions) ([]Account, ResultInfo, error) {
+	v := url.Values{}
+	if pageOpts.PerPage > 0 {
+		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
+	}
+	if pageOpts.Page > 0 {
+		v.Set("page", strconv.Itoa(pageOpts.Page))
+	}
+
+	uri := "/accounts"
+	if len(v) > 0 {
+		uri = uri + "?" + v.Encode()
+	}
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []Account{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accListResponse AccountListResponse
+	err = json.Unmarshal(res, &accListResponse)
+	if err != nil {
+		return []Account{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return accListResponse.Result, accListResponse.ResultInfo, nil
+}
+
+// Account returns a single account based on the ID.
+//
+// API reference: https://api.cloudflare.com/#accounts-account-details
+func (api *API) Account(accountID string) (Account, ResultInfo, error) {
+	uri := "/accounts/" + accountID
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return Account{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accResponse AccountResponse
+	err = json.Unmarshal(res, &accResponse)
+	if err != nil {
+		return Account{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accResponse.Result, accResponse.ResultInfo, nil
+}
+
+// UpdateAccount allows management of an account using the account ID.
+//
+// API reference: https://api.cloudflare.com/#accounts-update-account
+func (api *API) UpdateAccount(accountID string, account Account) (Account, error) {
+	uri := "/accounts/" + accountID
+
+	res, err := api.makeRequest("PUT", uri, account)
+	if err != nil {
+		return Account{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var a AccountDetailResponse
+	err = json.Unmarshal(res, &a)
+	if err != nil {
+		return Account{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return a.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/argo.go b/vendor/github.com/cloudflare/cloudflare-go/argo.go
new file mode 100644
index 000000000..320c7fc25
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/argo.go
@@ -0,0 +1,120 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+var validSettingValues = []string{"on", "off"}
+
+// ArgoFeatureSetting is the structure of the API object for the
+// argo smart routing and tiered caching settings.
+type ArgoFeatureSetting struct {
+	Editable   bool      `json:"editable,omitempty"`
+	ID         string    `json:"id,omitempty"`
+	ModifiedOn time.Time `json:"modified_on,omitempty"`
+	Value      string    `json:"value"`
+}
+
+// ArgoDetailsResponse is the API response for the argo smart routing
+// and tiered caching response.
+type ArgoDetailsResponse struct {
+	Result ArgoFeatureSetting `json:"result"`
+	Response
+}
+
+// ArgoSmartRouting returns the current settings for smart routing.
+//
+// API reference: https://api.cloudflare.com/#argo-smart-routing-get-argo-smart-routing-setting
+func (api *API) ArgoSmartRouting(zoneID string) (ArgoFeatureSetting, error) {
+	uri := "/zones/" + zoneID + "/argo/smart_routing"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var argoDetailsResponse ArgoDetailsResponse
+	err = json.Unmarshal(res, &argoDetailsResponse)
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return argoDetailsResponse.Result, nil
+}
+
+// UpdateArgoSmartRouting updates the setting for smart routing.
+//
+// API reference: https://api.cloudflare.com/#argo-smart-routing-patch-argo-smart-routing-setting
+func (api *API) UpdateArgoSmartRouting(zoneID, settingValue string) (ArgoFeatureSetting, error) {
+	if !contains(validSettingValues, settingValue) {
+		return ArgoFeatureSetting{}, errors.New(fmt.Sprintf("invalid setting value '%s'. must be 'on' or 'off'", settingValue))
+	}
+
+	uri := "/zones/" + zoneID + "/argo/smart_routing"
+
+	res, err := api.makeRequest("PATCH", uri, ArgoFeatureSetting{Value: settingValue})
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var argoDetailsResponse ArgoDetailsResponse
+	err = json.Unmarshal(res, &argoDetailsResponse)
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return argoDetailsResponse.Result, nil
+}
+
+// ArgoTieredCaching returns the current settings for tiered caching.
+//
+// API reference: TBA
+func (api *API) ArgoTieredCaching(zoneID string) (ArgoFeatureSetting, error) {
+	uri := "/zones/" + zoneID + "/argo/tiered_caching"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var argoDetailsResponse ArgoDetailsResponse
+	err = json.Unmarshal(res, &argoDetailsResponse)
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return argoDetailsResponse.Result, nil
+}
+
+// UpdateArgoTieredCaching updates the setting for tiered caching.
+//
+// API reference: TBA
+func (api *API) UpdateArgoTieredCaching(zoneID, settingValue string) (ArgoFeatureSetting, error) {
+	if !contains(validSettingValues, settingValue) {
+		return ArgoFeatureSetting{}, errors.New(fmt.Sprintf("invalid setting value '%s'. must be 'on' or 'off'", settingValue))
+	}
+
+	uri := "/zones/" + zoneID + "/argo/tiered_caching"
+
+	res, err := api.makeRequest("PATCH", uri, ArgoFeatureSetting{Value: settingValue})
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var argoDetailsResponse ArgoDetailsResponse
+	err = json.Unmarshal(res, &argoDetailsResponse)
+	if err != nil {
+		return ArgoFeatureSetting{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return argoDetailsResponse.Result, nil
+}
+
+func contains(s []string, e string) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/auditlogs.go b/vendor/github.com/cloudflare/cloudflare-go/auditlogs.go
new file mode 100644
index 000000000..8cb8eab69
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/auditlogs.go
@@ -0,0 +1,143 @@
+package cloudflare
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+// AuditLogAction is a member of AuditLog, the action that was taken.
+type AuditLogAction struct {
+	Result bool   `json:"result"`
+	Type   string `json:"type"`
+}
+
+// AuditLogActor is a member of AuditLog, who performed the action.
+type AuditLogActor struct {
+	Email string `json:"email"`
+	ID    string `json:"id"`
+	IP    string `json:"ip"`
+	Type  string `json:"type"`
+}
+
+// AuditLogOwner is a member of AuditLog, who owns this audit log.
+type AuditLogOwner struct {
+	ID string `json:"id"`
+}
+
+// AuditLogResource is a member of AuditLog, what was the action performed on.
+type AuditLogResource struct {
+	ID   string `json:"id"`
+	Type string `json:"type"`
+}
+
+// AuditLog is an resource that represents an update in the cloudflare dash
+type AuditLog struct {
+	Action   AuditLogAction         `json:"action"`
+	Actor    AuditLogActor          `json:"actor"`
+	ID       string                 `json:"id"`
+	Metadata map[string]interface{} `json:"metadata"`
+	NewValue string                 `json:"newValue"`
+	OldValue string                 `json:"oldValue"`
+	Owner    AuditLogOwner          `json:"owner"`
+	Resource AuditLogResource       `json:"resource"`
+	When     time.Time              `json:"when"`
+}
+
+// AuditLogResponse is the response returned from the cloudflare v4 api
+type AuditLogResponse struct {
+	Response   Response
+	Result     []AuditLog `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// AuditLogFilter is an object for filtering the audit log response from the api.
+type AuditLogFilter struct {
+	ID         string
+	ActorIP    string
+	ActorEmail string
+	Direction  string
+	ZoneName   string
+	Since      string
+	Before     string
+	PerPage    int
+	Page       int
+}
+
+// String turns an audit log filter in to an HTTP Query Param
+// list. It will not inclue empty members of the struct in the
+// query parameters.
+func (a AuditLogFilter) String() string {
+	params := "?"
+	if a.ID != "" {
+		params += "&id=" + a.ID
+	}
+	if a.ActorIP != "" {
+		params += "&actor.ip=" + a.ActorIP
+	}
+	if a.ActorEmail != "" {
+		params += "&actor.email=" + a.ActorEmail
+	}
+	if a.ZoneName != "" {
+		params += "&zone.name=" + a.ZoneName
+	}
+	if a.Direction != "" {
+		params += "&direction=" + a.Direction
+	}
+	if a.Since != "" {
+		params += "&since=" + a.Since
+	}
+	if a.Before != "" {
+		params += "&before=" + a.Before
+	}
+	if a.PerPage > 0 {
+		params += "&per_page=" + fmt.Sprintf("%d", a.PerPage)
+	}
+	if a.Page > 0 {
+		params += "&page=" + fmt.Sprintf("%d", a.Page)
+	}
+	return params
+}
+
+// GetOrganizationAuditLogs will return the audit logs of a specific
+// organization, based on the ID passed in. The audit logs can be
+// filtered based on any argument in the AuditLogFilter
+//
+// API Reference: https://api.cloudflare.com/#audit-logs-list-organization-audit-logs
+func (api *API) GetOrganizationAuditLogs(organizationID string, a AuditLogFilter) (AuditLogResponse, error) {
+	uri := "/organizations/" + organizationID + "/audit_logs" + fmt.Sprintf("%s", a)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AuditLogResponse{}, err
+	}
+	buf, err := base64.RawStdEncoding.DecodeString(string(res))
+	if err != nil {
+		return AuditLogResponse{}, err
+	}
+	return unmarshalReturn(buf)
+}
+
+// unmarshalReturn will unmarshal bytes and return an auditlogresponse
+func unmarshalReturn(res []byte) (AuditLogResponse, error) {
+	var auditResponse AuditLogResponse
+	err := json.Unmarshal(res, &auditResponse)
+	if err != nil {
+		return auditResponse, err
+	}
+	return auditResponse, nil
+}
+
+// GetUserAuditLogs will return your user's audit logs. The audit logs can be
+// filtered based on any argument in the AuditLogFilter
+//
+// API Reference: https://api.cloudflare.com/#audit-logs-list-user-audit-logs
+func (api *API) GetUserAuditLogs(a AuditLogFilter) (AuditLogResponse, error) {
+	uri := "/user/audit_logs" + fmt.Sprintf("%s", a)
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return AuditLogResponse{}, err
+	}
+	return unmarshalReturn(res)
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go b/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go
new file mode 100644
index 000000000..498b9f468
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go
@@ -0,0 +1,435 @@
+// Package cloudflare implements the Cloudflare v4 API.
+package cloudflare
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"log"
+	"math"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+	"golang.org/x/time/rate"
+)
+
+const apiURL = "https://api.cloudflare.com/client/v4"
+
+const (
+	// AuthKeyEmail specifies that we should authenticate with API key and email address
+	AuthKeyEmail = 1 << iota
+	// AuthUserService specifies that we should authenticate with a User-Service key
+	AuthUserService
+	// AuthToken specifies that we should authenticate with an API Token
+	AuthToken
+)
+
+// API holds the configuration for the current API client. A client should not
+// be modified concurrently.
+type API struct {
+	APIKey            string
+	APIEmail          string
+	APIUserServiceKey string
+	APIToken          string
+	BaseURL           string
+	AccountID         string
+	UserAgent         string
+	headers           http.Header
+	httpClient        *http.Client
+	authType          int
+	rateLimiter       *rate.Limiter
+	retryPolicy       RetryPolicy
+	logger            Logger
+}
+
+// newClient provides shared logic for New and NewWithUserServiceKey
+func newClient(opts ...Option) (*API, error) {
+	silentLogger := log.New(ioutil.Discard, "", log.LstdFlags)
+
+	api := &API{
+		BaseURL:     apiURL,
+		headers:     make(http.Header),
+		rateLimiter: rate.NewLimiter(rate.Limit(4), 1), // 4rps equates to default api limit (1200 req/5 min)
+		retryPolicy: RetryPolicy{
+			MaxRetries:    3,
+			MinRetryDelay: time.Duration(1) * time.Second,
+			MaxRetryDelay: time.Duration(30) * time.Second,
+		},
+		logger: silentLogger,
+	}
+
+	err := api.parseOptions(opts...)
+	if err != nil {
+		return nil, errors.Wrap(err, "options parsing failed")
+	}
+
+	// Fall back to http.DefaultClient if the package user does not provide
+	// their own.
+	if api.httpClient == nil {
+		api.httpClient = http.DefaultClient
+	}
+
+	return api, nil
+}
+
+// New creates a new Cloudflare v4 API client.
+func New(key, email string, opts ...Option) (*API, error) {
+	if key == "" || email == "" {
+		return nil, errors.New(errEmptyCredentials)
+	}
+
+	api, err := newClient(opts...)
+	if err != nil {
+		return nil, err
+	}
+
+	api.APIKey = key
+	api.APIEmail = email
+	api.authType = AuthKeyEmail
+
+	return api, nil
+}
+
+// NewWithAPIToken creates a new Cloudflare v4 API client using API Tokens
+func NewWithAPIToken(token string, opts ...Option) (*API, error) {
+	if token == "" {
+		return nil, errors.New(errEmptyAPIToken)
+	}
+
+	api, err := newClient(opts...)
+	if err != nil {
+		return nil, err
+	}
+
+	api.APIToken = token
+	api.authType = AuthToken
+
+	return api, nil
+}
+
+// NewWithUserServiceKey creates a new Cloudflare v4 API client using service key authentication.
+func NewWithUserServiceKey(key string, opts ...Option) (*API, error) {
+	if key == "" {
+		return nil, errors.New(errEmptyCredentials)
+	}
+
+	api, err := newClient(opts...)
+	if err != nil {
+		return nil, err
+	}
+
+	api.APIUserServiceKey = key
+	api.authType = AuthUserService
+
+	return api, nil
+}
+
+// SetAuthType sets the authentication method (AuthKeyEmail, AuthToken, or AuthUserService).
+func (api *API) SetAuthType(authType int) {
+	api.authType = authType
+}
+
+// ZoneIDByName retrieves a zone's ID from the name.
+func (api *API) ZoneIDByName(zoneName string) (string, error) {
+	res, err := api.ListZonesContext(context.TODO(), WithZoneFilter(zoneName))
+	if err != nil {
+		return "", errors.Wrap(err, "ListZonesContext command failed")
+	}
+
+	if len(res.Result) > 1 && api.AccountID == "" {
+		return "", errors.New("ambiguous zone name used without an account ID")
+	}
+
+	for _, zone := range res.Result {
+		if api.AccountID != "" {
+			if zone.Name == zoneName && api.AccountID == zone.Account.ID {
+				return zone.ID, nil
+			}
+		} else {
+			if zone.Name == zoneName {
+				return zone.ID, nil
+			}
+		}
+	}
+
+	return "", errors.New("Zone could not be found")
+}
+
+// makeRequest makes a HTTP request and returns the body as a byte slice,
+// closing it before returning. params will be serialized to JSON.
+func (api *API) makeRequest(method, uri string, params interface{}) ([]byte, error) {
+	return api.makeRequestWithAuthType(context.TODO(), method, uri, params, api.authType)
+}
+
+func (api *API) makeRequestContext(ctx context.Context, method, uri string, params interface{}) ([]byte, error) {
+	return api.makeRequestWithAuthType(ctx, method, uri, params, api.authType)
+}
+
+func (api *API) makeRequestWithHeaders(method, uri string, params interface{}, headers http.Header) ([]byte, error) {
+	return api.makeRequestWithAuthTypeAndHeaders(context.TODO(), method, uri, params, api.authType, headers)
+}
+
+func (api *API) makeRequestWithAuthType(ctx context.Context, method, uri string, params interface{}, authType int) ([]byte, error) {
+	return api.makeRequestWithAuthTypeAndHeaders(ctx, method, uri, params, authType, nil)
+}
+
+func (api *API) makeRequestWithAuthTypeAndHeaders(ctx context.Context, method, uri string, params interface{}, authType int, headers http.Header) ([]byte, error) {
+	// Replace nil with a JSON object if needed
+	var jsonBody []byte
+	var err error
+
+	if params != nil {
+		if paramBytes, ok := params.([]byte); ok {
+			jsonBody = paramBytes
+		} else {
+			jsonBody, err = json.Marshal(params)
+			if err != nil {
+				return nil, errors.Wrap(err, "error marshalling params to JSON")
+			}
+		}
+	} else {
+		jsonBody = nil
+	}
+
+	var resp *http.Response
+	var respErr error
+	var reqBody io.Reader
+	var respBody []byte
+	for i := 0; i <= api.retryPolicy.MaxRetries; i++ {
+		if jsonBody != nil {
+			reqBody = bytes.NewReader(jsonBody)
+		}
+		if i > 0 {
+			// expect the backoff introduced here on errored requests to dominate the effect of rate limiting
+			// don't need a random component here as the rate limiter should do something similar
+			// nb time duration could truncate an arbitrary float. Since our inputs are all ints, we should be ok
+			sleepDuration := time.Duration(math.Pow(2, float64(i-1)) * float64(api.retryPolicy.MinRetryDelay))
+
+			if sleepDuration > api.retryPolicy.MaxRetryDelay {
+				sleepDuration = api.retryPolicy.MaxRetryDelay
+			}
+			// useful to do some simple logging here, maybe introduce levels later
+			api.logger.Printf("Sleeping %s before retry attempt number %d for request %s %s", sleepDuration.String(), i, method, uri)
+			time.Sleep(sleepDuration)
+
+		}
+		err = api.rateLimiter.Wait(context.TODO())
+		if err != nil {
+			return nil, errors.Wrap(err, "Error caused by request rate limiting")
+		}
+		resp, respErr = api.request(ctx, method, uri, reqBody, authType, headers)
+
+		// retry if the server is rate limiting us or if it failed
+		// assumes server operations are rolled back on failure
+		if respErr != nil || resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
+			// if we got a valid http response, try to read body so we can reuse the connection
+			// see https://golang.org/pkg/net/http/#Client.Do
+			if respErr == nil {
+				respBody, err = ioutil.ReadAll(resp.Body)
+				resp.Body.Close()
+
+				respErr = errors.Wrap(err, "could not read response body")
+
+				api.logger.Printf("Request: %s %s got an error response %d: %s\n", method, uri, resp.StatusCode,
+					strings.Replace(strings.Replace(string(respBody), "\n", "", -1), "\t", "", -1))
+			} else {
+				api.logger.Printf("Error performing request: %s %s : %s \n", method, uri, respErr.Error())
+			}
+			continue
+		} else {
+			respBody, err = ioutil.ReadAll(resp.Body)
+			defer resp.Body.Close()
+			if err != nil {
+				return nil, errors.Wrap(err, "could not read response body")
+			}
+			break
+		}
+	}
+	if respErr != nil {
+		return nil, respErr
+	}
+
+	switch {
+	case resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices:
+	case resp.StatusCode == http.StatusUnauthorized:
+		return nil, errors.Errorf("HTTP status %d: invalid credentials", resp.StatusCode)
+	case resp.StatusCode == http.StatusForbidden:
+		return nil, errors.Errorf("HTTP status %d: insufficient permissions", resp.StatusCode)
+	case resp.StatusCode == http.StatusServiceUnavailable,
+		resp.StatusCode == http.StatusBadGateway,
+		resp.StatusCode == http.StatusGatewayTimeout,
+		resp.StatusCode == 522,
+		resp.StatusCode == 523,
+		resp.StatusCode == 524:
+		return nil, errors.Errorf("HTTP status %d: service failure", resp.StatusCode)
+	// This isn't a great solution due to the way the `default` case is
+	// a catch all and that the `filters/validate-expr` returns a HTTP 400
+	// yet the clients need to use the HTTP body as a JSON string.
+	case resp.StatusCode == 400 && strings.HasSuffix(resp.Request.URL.Path, "/filters/validate-expr"):
+		return nil, errors.Errorf("%s", respBody)
+	default:
+		var s string
+		if respBody != nil {
+			s = string(respBody)
+		}
+		return nil, errors.Errorf("HTTP status %d: content %q", resp.StatusCode, s)
+	}
+
+	return respBody, nil
+}
+
+// request makes a HTTP request to the given API endpoint, returning the raw
+// *http.Response, or an error if one occurred. The caller is responsible for
+// closing the response body.
+func (api *API) request(ctx context.Context, method, uri string, reqBody io.Reader, authType int, headers http.Header) (*http.Response, error) {
+	req, err := http.NewRequest(method, api.BaseURL+uri, reqBody)
+	if err != nil {
+		return nil, errors.Wrap(err, "HTTP request creation failed")
+	}
+	req.WithContext(ctx)
+
+	combinedHeaders := make(http.Header)
+	copyHeader(combinedHeaders, api.headers)
+	copyHeader(combinedHeaders, headers)
+	req.Header = combinedHeaders
+
+	if authType&AuthKeyEmail != 0 {
+		req.Header.Set("X-Auth-Key", api.APIKey)
+		req.Header.Set("X-Auth-Email", api.APIEmail)
+	}
+	if authType&AuthUserService != 0 {
+		req.Header.Set("X-Auth-User-Service-Key", api.APIUserServiceKey)
+	}
+	if authType&AuthToken != 0 {
+		req.Header.Set("Authorization", "Bearer "+api.APIToken)
+	}
+
+	if api.UserAgent != "" {
+		req.Header.Set("User-Agent", api.UserAgent)
+	}
+
+	if req.Header.Get("Content-Type") == "" {
+		req.Header.Set("Content-Type", "application/json")
+	}
+
+	resp, err := api.httpClient.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "HTTP request failed")
+	}
+
+	return resp, nil
+}
+
+// Returns the base URL to use for API endpoints that exist for accounts.
+// If an account option was used when creating the API instance, returns
+// the account URL.
+//
+// accountBase is the base URL for endpoints referring to the current user.
+// It exists as a parameter because it is not consistent across APIs.
+func (api *API) userBaseURL(accountBase string) string {
+	if api.AccountID != "" {
+		return "/accounts/" + api.AccountID
+	}
+	return accountBase
+}
+
+// copyHeader copies all headers for `source` and sets them on `target`.
+// based on https://godoc.org/github.com/golang/gddo/httputil/header#Copy
+func copyHeader(target, source http.Header) {
+	for k, vs := range source {
+		target[k] = vs
+	}
+}
+
+// ResponseInfo contains a code and message returned by the API as errors or
+// informational messages inside the response.
+type ResponseInfo struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+}
+
+// Response is a template.  There will also be a result struct.  There will be a
+// unique response type for each response, which will include this type.
+type Response struct {
+	Success  bool           `json:"success"`
+	Errors   []ResponseInfo `json:"errors"`
+	Messages []ResponseInfo `json:"messages"`
+}
+
+// ResultInfo contains metadata about the Response.
+type ResultInfo struct {
+	Page       int `json:"page"`
+	PerPage    int `json:"per_page"`
+	TotalPages int `json:"total_pages"`
+	Count      int `json:"count"`
+	Total      int `json:"total_count"`
+}
+
+// RawResponse keeps the result as JSON form
+type RawResponse struct {
+	Response
+	Result json.RawMessage `json:"result"`
+}
+
+// Raw makes a HTTP request with user provided params and returns the
+// result as untouched JSON.
+func (api *API) Raw(method, endpoint string, data interface{}) (json.RawMessage, error) {
+	res, err := api.makeRequest(method, endpoint, data)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r RawResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// PaginationOptions can be passed to a list request to configure paging
+// These values will be defaulted if omitted, and PerPage has min/max limits set by resource
+type PaginationOptions struct {
+	Page    int `json:"page,omitempty"`
+	PerPage int `json:"per_page,omitempty"`
+}
+
+// RetryPolicy specifies number of retries and min/max retry delays
+// This config is used when the client exponentially backs off after errored requests
+type RetryPolicy struct {
+	MaxRetries    int
+	MinRetryDelay time.Duration
+	MaxRetryDelay time.Duration
+}
+
+// Logger defines the interface this library needs to use logging
+// This is a subset of the methods implemented in the log package
+type Logger interface {
+	Printf(format string, v ...interface{})
+}
+
+// ReqOption is a functional option for configuring API requests
+type ReqOption func(opt *reqOption)
+type reqOption struct {
+	params url.Values
+}
+
+// WithZoneFilter applies a filter based on zone name.
+func WithZoneFilter(zone string) ReqOption {
+	return func(opt *reqOption) {
+		opt.params.Set("name", zone)
+	}
+}
+
+// WithPagination configures the pagination for a response.
+func WithPagination(opts PaginationOptions) ReqOption {
+	return func(opt *reqOption) {
+		opt.params.Set("page", strconv.Itoa(opts.Page))
+		opt.params.Set("per_page", strconv.Itoa(opts.PerPage))
+	}
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/custom_hostname.go b/vendor/github.com/cloudflare/cloudflare-go/custom_hostname.go
new file mode 100644
index 000000000..d982c5b50
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/custom_hostname.go
@@ -0,0 +1,161 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+// CustomHostnameSSLSettings represents the SSL settings for a custom hostname.
+type CustomHostnameSSLSettings struct {
+	HTTP2         string   `json:"http2,omitempty"`
+	TLS13         string   `json:"tls_1_3,omitempty"`
+	MinTLSVersion string   `json:"min_tls_version,omitempty"`
+	Ciphers       []string `json:"ciphers,omitempty"`
+}
+
+// CustomHostnameSSL represents the SSL section in a given custom hostname.
+type CustomHostnameSSL struct {
+	Status      string                    `json:"status,omitempty"`
+	Method      string                    `json:"method,omitempty"`
+	Type        string                    `json:"type,omitempty"`
+	CnameTarget string                    `json:"cname_target,omitempty"`
+	CnameName   string                    `json:"cname,omitempty"`
+	Settings    CustomHostnameSSLSettings `json:"settings,omitempty"`
+}
+
+// CustomMetadata defines custom metadata for the hostname. This requires logic to be implemented by Cloudflare to act on the data provided.
+type CustomMetadata map[string]interface{}
+
+// CustomHostname represents a custom hostname in a zone.
+type CustomHostname struct {
+	ID                 string            `json:"id,omitempty"`
+	Hostname           string            `json:"hostname,omitempty"`
+	CustomOriginServer string            `json:"custom_origin_server,omitempty"`
+	SSL                CustomHostnameSSL `json:"ssl,omitempty"`
+	CustomMetadata     CustomMetadata    `json:"custom_metadata,omitempty"`
+}
+
+// CustomHostnameResponse represents a response from the Custom Hostnames endpoints.
+type CustomHostnameResponse struct {
+	Result CustomHostname `json:"result"`
+	Response
+}
+
+// CustomHostnameListResponse represents a response from the Custom Hostnames endpoints.
+type CustomHostnameListResponse struct {
+	Result []CustomHostname `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// UpdateCustomHostnameSSL modifies SSL configuration for the given custom
+// hostname in the given zone.
+//
+// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-update-custom-hostname-configuration
+func (api *API) UpdateCustomHostnameSSL(zoneID string, customHostnameID string, ssl CustomHostnameSSL) (CustomHostname, error) {
+	return CustomHostname{}, errors.New("Not implemented")
+}
+
+// DeleteCustomHostname deletes a custom hostname (and any issued SSL
+// certificates).
+//
+// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-delete-a-custom-hostname-and-any-issued-ssl-certificates-
+func (api *API) DeleteCustomHostname(zoneID string, customHostnameID string) error {
+	uri := "/zones/" + zoneID + "/custom_hostnames/" + customHostnameID
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	var response *CustomHostnameResponse
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+
+	return nil
+}
+
+// CreateCustomHostname creates a new custom hostname and requests that an SSL certificate be issued for it.
+//
+// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-create-custom-hostname
+func (api *API) CreateCustomHostname(zoneID string, ch CustomHostname) (*CustomHostnameResponse, error) {
+	uri := "/zones/" + zoneID + "/custom_hostnames"
+	res, err := api.makeRequest("POST", uri, ch)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var response *CustomHostnameResponse
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// CustomHostnames fetches custom hostnames for the given zone,
+// by applying filter.Hostname if not empty and scoping the result to page'th 50 items.
+//
+// The returned ResultInfo can be used to implement pagination.
+//
+// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-list-custom-hostnames
+func (api *API) CustomHostnames(zoneID string, page int, filter CustomHostname) ([]CustomHostname, ResultInfo, error) {
+	v := url.Values{}
+	v.Set("per_page", "50")
+	v.Set("page", strconv.Itoa(page))
+	if filter.Hostname != "" {
+		v.Set("hostname", filter.Hostname)
+	}
+	query := "?" + v.Encode()
+
+	uri := "/zones/" + zoneID + "/custom_hostnames" + query
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []CustomHostname{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var customHostnameListResponse CustomHostnameListResponse
+	err = json.Unmarshal(res, &customHostnameListResponse)
+	if err != nil {
+		return []CustomHostname{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	return customHostnameListResponse.Result, customHostnameListResponse.ResultInfo, nil
+}
+
+// CustomHostname inspects the given custom hostname in the given zone.
+//
+// API reference: https://api.cloudflare.com/#custom-hostname-for-a-zone-custom-hostname-configuration-details
+func (api *API) CustomHostname(zoneID string, customHostnameID string) (CustomHostname, error) {
+	uri := "/zones/" + zoneID + "/custom_hostnames/" + customHostnameID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return CustomHostname{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var response CustomHostnameResponse
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return CustomHostname{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response.Result, nil
+}
+
+// CustomHostnameIDByName retrieves the ID for the given hostname in the given zone.
+func (api *API) CustomHostnameIDByName(zoneID string, hostname string) (string, error) {
+	customHostnames, _, err := api.CustomHostnames(zoneID, 1, CustomHostname{Hostname: hostname})
+	if err != nil {
+		return "", errors.Wrap(err, "CustomHostnames command failed")
+	}
+	for _, ch := range customHostnames {
+		if ch.Hostname == hostname {
+			return ch.ID, nil
+		}
+	}
+	return "", errors.New("CustomHostname could not be found")
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/custom_pages.go b/vendor/github.com/cloudflare/cloudflare-go/custom_pages.go
new file mode 100644
index 000000000..d96788fc8
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/custom_pages.go
@@ -0,0 +1,176 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// CustomPage represents a custom page configuration.
+type CustomPage struct {
+	CreatedOn      time.Time   `json:"created_on"`
+	ModifiedOn     time.Time   `json:"modified_on"`
+	URL            interface{} `json:"url"`
+	State          string      `json:"state"`
+	RequiredTokens []string    `json:"required_tokens"`
+	PreviewTarget  string      `json:"preview_target"`
+	Description    string      `json:"description"`
+	ID             string      `json:"id"`
+}
+
+// CustomPageResponse represents the response from the custom pages endpoint.
+type CustomPageResponse struct {
+	Response
+	Result []CustomPage `json:"result"`
+}
+
+// CustomPageDetailResponse represents the response from the custom page endpoint.
+type CustomPageDetailResponse struct {
+	Response
+	Result CustomPage `json:"result"`
+}
+
+// CustomPageOptions is used to determine whether or not the operation
+// should take place on an account or zone level based on which is
+// provided to the function.
+//
+// A non-empty value denotes desired use.
+type CustomPageOptions struct {
+	AccountID string
+	ZoneID    string
+}
+
+// CustomPageParameters is used to update a particular custom page with
+// the values provided.
+type CustomPageParameters struct {
+	URL   interface{} `json:"url"`
+	State string      `json:"state"`
+}
+
+// CustomPages lists custom pages for a zone or account.
+//
+// Zone API reference: https://api.cloudflare.com/#custom-pages-for-a-zone-list-available-custom-pages
+// Account API reference: https://api.cloudflare.com/#custom-pages-account--list-custom-pages
+func (api *API) CustomPages(options *CustomPageOptions) ([]CustomPage, error) {
+	var (
+		pageType, identifier string
+	)
+
+	if options.AccountID == "" && options.ZoneID == "" {
+		return nil, errors.New("either account ID or zone ID must be provided")
+	}
+
+	if options.AccountID != "" && options.ZoneID != "" {
+		return nil, errors.New("account ID and zone ID are mutually exclusive")
+	}
+
+	// Should the account ID be defined, treat this as an account level operation.
+	if options.AccountID != "" {
+		pageType = "accounts"
+		identifier = options.AccountID
+	} else {
+		pageType = "zones"
+		identifier = options.ZoneID
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_pages", pageType, identifier)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var customPageResponse CustomPageResponse
+	err = json.Unmarshal(res, &customPageResponse)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return customPageResponse.Result, nil
+}
+
+// CustomPage lists a single custom page based on the ID.
+//
+// Zone API reference: https://api.cloudflare.com/#custom-pages-for-a-zone-custom-page-details
+// Account API reference: https://api.cloudflare.com/#custom-pages-account--custom-page-details
+func (api *API) CustomPage(options *CustomPageOptions, customPageID string) (CustomPage, error) {
+	var (
+		pageType, identifier string
+	)
+
+	if options.AccountID == "" && options.ZoneID == "" {
+		return CustomPage{}, errors.New("either account ID or zone ID must be provided")
+	}
+
+	if options.AccountID != "" && options.ZoneID != "" {
+		return CustomPage{}, errors.New("account ID and zone ID are mutually exclusive")
+	}
+
+	// Should the account ID be defined, treat this as an account level operation.
+	if options.AccountID != "" {
+		pageType = "accounts"
+		identifier = options.AccountID
+	} else {
+		pageType = "zones"
+		identifier = options.ZoneID
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_pages/%s", pageType, identifier, customPageID)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return CustomPage{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var customPageResponse CustomPageDetailResponse
+	err = json.Unmarshal(res, &customPageResponse)
+	if err != nil {
+		return CustomPage{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return customPageResponse.Result, nil
+}
+
+// UpdateCustomPage updates a single custom page setting.
+//
+// Zone API reference: https://api.cloudflare.com/#custom-pages-for-a-zone-update-custom-page-url
+// Account API reference: https://api.cloudflare.com/#custom-pages-account--update-custom-page
+func (api *API) UpdateCustomPage(options *CustomPageOptions, customPageID string, pageParameters CustomPageParameters) (CustomPage, error) {
+	var (
+		pageType, identifier string
+	)
+
+	if options.AccountID == "" && options.ZoneID == "" {
+		return CustomPage{}, errors.New("either account ID or zone ID must be provided")
+	}
+
+	if options.AccountID != "" && options.ZoneID != "" {
+		return CustomPage{}, errors.New("account ID and zone ID are mutually exclusive")
+	}
+
+	// Should the account ID be defined, treat this as an account level operation.
+	if options.AccountID != "" {
+		pageType = "accounts"
+		identifier = options.AccountID
+	} else {
+		pageType = "zones"
+		identifier = options.ZoneID
+	}
+
+	uri := fmt.Sprintf("/%s/%s/custom_pages/%s", pageType, identifier, customPageID)
+
+	res, err := api.makeRequest("PUT", uri, pageParameters)
+	if err != nil {
+		return CustomPage{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var customPageResponse CustomPageDetailResponse
+	err = json.Unmarshal(res, &customPageResponse)
+	if err != nil {
+		return CustomPage{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return customPageResponse.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/dns.go b/vendor/github.com/cloudflare/cloudflare-go/dns.go
new file mode 100644
index 000000000..6bcac2480
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/dns.go
@@ -0,0 +1,174 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// DNSRecord represents a DNS record in a zone.
+type DNSRecord struct {
+	ID         string      `json:"id,omitempty"`
+	Type       string      `json:"type,omitempty"`
+	Name       string      `json:"name,omitempty"`
+	Content    string      `json:"content,omitempty"`
+	Proxiable  bool        `json:"proxiable,omitempty"`
+	Proxied    bool        `json:"proxied"`
+	TTL        int         `json:"ttl,omitempty"`
+	Locked     bool        `json:"locked,omitempty"`
+	ZoneID     string      `json:"zone_id,omitempty"`
+	ZoneName   string      `json:"zone_name,omitempty"`
+	CreatedOn  time.Time   `json:"created_on,omitempty"`
+	ModifiedOn time.Time   `json:"modified_on,omitempty"`
+	Data       interface{} `json:"data,omitempty"` // data returned by: SRV, LOC
+	Meta       interface{} `json:"meta,omitempty"`
+	Priority   int         `json:"priority"`
+}
+
+// DNSRecordResponse represents the response from the DNS endpoint.
+type DNSRecordResponse struct {
+	Result DNSRecord `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// DNSListResponse represents the response from the list DNS records endpoint.
+type DNSListResponse struct {
+	Result []DNSRecord `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// CreateDNSRecord creates a DNS record for the zone identifier.
+//
+// API reference: https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record
+func (api *API) CreateDNSRecord(zoneID string, rr DNSRecord) (*DNSRecordResponse, error) {
+	uri := "/zones/" + zoneID + "/dns_records"
+	res, err := api.makeRequest("POST", uri, rr)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var recordResp *DNSRecordResponse
+	err = json.Unmarshal(res, &recordResp)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return recordResp, nil
+}
+
+// DNSRecords returns a slice of DNS records for the given zone identifier.
+//
+// This takes a DNSRecord to allow filtering of the results returned.
+//
+// API reference: https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
+func (api *API) DNSRecords(zoneID string, rr DNSRecord) ([]DNSRecord, error) {
+	// Construct a query string
+	v := url.Values{}
+	// Request as many records as possible per page - API max is 50
+	v.Set("per_page", "50")
+	if rr.Name != "" {
+		v.Set("name", rr.Name)
+	}
+	if rr.Type != "" {
+		v.Set("type", rr.Type)
+	}
+	if rr.Content != "" {
+		v.Set("content", rr.Content)
+	}
+
+	var query string
+	var records []DNSRecord
+	page := 1
+
+	// Loop over makeRequest until what we've fetched all records
+	for {
+		v.Set("page", strconv.Itoa(page))
+		query = "?" + v.Encode()
+		uri := "/zones/" + zoneID + "/dns_records" + query
+		res, err := api.makeRequest("GET", uri, nil)
+		if err != nil {
+			return []DNSRecord{}, errors.Wrap(err, errMakeRequestError)
+		}
+		var r DNSListResponse
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return []DNSRecord{}, errors.Wrap(err, errUnmarshalError)
+		}
+		records = append(records, r.Result...)
+		if r.ResultInfo.Page >= r.ResultInfo.TotalPages {
+			break
+		}
+		// Loop around and fetch the next page
+		page++
+	}
+	return records, nil
+}
+
+// DNSRecord returns a single DNS record for the given zone & record
+// identifiers.
+//
+// API reference: https://api.cloudflare.com/#dns-records-for-a-zone-dns-record-details
+func (api *API) DNSRecord(zoneID, recordID string) (DNSRecord, error) {
+	uri := "/zones/" + zoneID + "/dns_records/" + recordID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return DNSRecord{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r DNSRecordResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return DNSRecord{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// UpdateDNSRecord updates a single DNS record for the given zone & record
+// identifiers.
+//
+// API reference: https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record
+func (api *API) UpdateDNSRecord(zoneID, recordID string, rr DNSRecord) error {
+	rec, err := api.DNSRecord(zoneID, recordID)
+	if err != nil {
+		return err
+	}
+	// Populate the record name from the existing one if the update didn't
+	// specify it.
+	if rr.Name == "" {
+		rr.Name = rec.Name
+	}
+	rr.Type = rec.Type
+	uri := "/zones/" + zoneID + "/dns_records/" + recordID
+	res, err := api.makeRequest("PATCH", uri, rr)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r DNSRecordResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
+
+// DeleteDNSRecord deletes a single DNS record for the given zone & record
+// identifiers.
+//
+// API reference: https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record
+func (api *API) DeleteDNSRecord(zoneID, recordID string) error {
+	uri := "/zones/" + zoneID + "/dns_records/" + recordID
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r DNSRecordResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/duration.go b/vendor/github.com/cloudflare/cloudflare-go/duration.go
new file mode 100644
index 000000000..ba2418acd
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/duration.go
@@ -0,0 +1,40 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+)
+
+// Duration implements json.Marshaler and json.Unmarshaler for time.Duration
+// using the fmt.Stringer interface of time.Duration and time.ParseDuration.
+type Duration struct {
+	time.Duration
+}
+
+// MarshalJSON encodes a Duration as a JSON string formatted using String.
+func (d Duration) MarshalJSON() ([]byte, error) {
+	return json.Marshal(d.Duration.String())
+}
+
+// UnmarshalJSON decodes a Duration from a JSON string parsed using time.ParseDuration.
+func (d *Duration) UnmarshalJSON(buf []byte) error {
+	var str string
+
+	err := json.Unmarshal(buf, &str)
+	if err != nil {
+		return err
+	}
+
+	dur, err := time.ParseDuration(str)
+	if err != nil {
+		return err
+	}
+
+	d.Duration = dur
+	return nil
+}
+
+var (
+	_ = json.Marshaler((*Duration)(nil))
+	_ = json.Unmarshaler((*Duration)(nil))
+)
diff --git a/vendor/github.com/cloudflare/cloudflare-go/errors.go b/vendor/github.com/cloudflare/cloudflare-go/errors.go
new file mode 100644
index 000000000..21c38b168
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/errors.go
@@ -0,0 +1,50 @@
+package cloudflare
+
+// Error messages
+const (
+	errEmptyCredentials     = "invalid credentials: key & email must not be empty"
+	errEmptyAPIToken        = "invalid credentials: API Token must not be empty"
+	errMakeRequestError     = "error from makeRequest"
+	errUnmarshalError       = "error unmarshalling the JSON response"
+	errRequestNotSuccessful = "error reported by API"
+	errMissingAccountID     = "account ID is empty and must be provided"
+)
+
+var _ Error = &UserError{}
+
+// Error represents an error returned from this library.
+type Error interface {
+	error
+	// Raised when user credentials or configuration is invalid.
+	User() bool
+	// Raised when a parsing error (e.g. JSON) occurs.
+	Parse() bool
+	// Raised when a network error occurs.
+	Network() bool
+	// Contains the most recent error.
+}
+
+// UserError represents a user-generated error.
+type UserError struct {
+	Err error
+}
+
+// User is a user-caused error.
+func (e *UserError) User() bool {
+	return true
+}
+
+// Network error.
+func (e *UserError) Network() bool {
+	return false
+}
+
+// Parse error.
+func (e *UserError) Parse() bool {
+	return true
+}
+
+// Error wraps the underlying error.
+func (e *UserError) Error() string {
+	return e.Err.Error()
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/filter.go b/vendor/github.com/cloudflare/cloudflare-go/filter.go
new file mode 100644
index 000000000..cf3ef1c20
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/filter.go
@@ -0,0 +1,241 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+)
+
+// Filter holds the structure of the filter type.
+type Filter struct {
+	ID          string `json:"id,omitempty"`
+	Expression  string `json:"expression"`
+	Paused      bool   `json:"paused"`
+	Description string `json:"description"`
+
+	// Property is mentioned in documentation however isn't populated in
+	// any of the API requests. For now, let's just omit it unless it's
+	// provided.
+	Ref string `json:"ref,omitempty"`
+}
+
+// FiltersDetailResponse is the API response that is returned
+// for requesting all filters on a zone.
+type FiltersDetailResponse struct {
+	Result     []Filter `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+// FilterDetailResponse is the API response that is returned
+// for requesting a single filter on a zone.
+type FilterDetailResponse struct {
+	Result     Filter `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+// FilterValidateExpression represents the JSON payload for checking
+// an expression.
+type FilterValidateExpression struct {
+	Expression string `json:"expression"`
+}
+
+// FilterValidateExpressionResponse represents the API response for
+// checking the expression. It conforms to the JSON API approach however
+// we don't need all of the fields exposed.
+type FilterValidateExpressionResponse struct {
+	Success bool                                `json:"success"`
+	Errors  []FilterValidationExpressionMessage `json:"errors"`
+}
+
+// FilterValidationExpressionMessage represents the API error message.
+type FilterValidationExpressionMessage struct {
+	Message string `json:"message"`
+}
+
+// Filter returns a single filter in a zone based on the filter ID.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-by-filter-id
+func (api *API) Filter(zoneID, filterID string) (Filter, error) {
+	uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filterID)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return Filter{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var filterResponse FilterDetailResponse
+	err = json.Unmarshal(res, &filterResponse)
+	if err != nil {
+		return Filter{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return filterResponse.Result, nil
+}
+
+// Filters returns all filters for a zone.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-all-filters
+func (api *API) Filters(zoneID string, pageOpts PaginationOptions) ([]Filter, error) {
+	uri := "/zones/" + zoneID + "/filters"
+	v := url.Values{}
+
+	if pageOpts.PerPage > 0 {
+		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
+	}
+
+	if pageOpts.Page > 0 {
+		v.Set("page", strconv.Itoa(pageOpts.Page))
+	}
+
+	if len(v) > 0 {
+		uri = uri + "?" + v.Encode()
+	}
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []Filter{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var filtersResponse FiltersDetailResponse
+	err = json.Unmarshal(res, &filtersResponse)
+	if err != nil {
+		return []Filter{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return filtersResponse.Result, nil
+}
+
+// CreateFilters creates new filters.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/post/
+func (api *API) CreateFilters(zoneID string, filters []Filter) ([]Filter, error) {
+	uri := "/zones/" + zoneID + "/filters"
+
+	res, err := api.makeRequest("POST", uri, filters)
+	if err != nil {
+		return []Filter{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var filtersResponse FiltersDetailResponse
+	err = json.Unmarshal(res, &filtersResponse)
+	if err != nil {
+		return []Filter{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return filtersResponse.Result, nil
+}
+
+// UpdateFilter updates a single filter.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-a-single-filter
+func (api *API) UpdateFilter(zoneID string, filter Filter) (Filter, error) {
+	if filter.ID == "" {
+		return Filter{}, errors.Errorf("filter ID cannot be empty")
+	}
+
+	uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filter.ID)
+
+	res, err := api.makeRequest("PUT", uri, filter)
+	if err != nil {
+		return Filter{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var filterResponse FilterDetailResponse
+	err = json.Unmarshal(res, &filterResponse)
+	if err != nil {
+		return Filter{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return filterResponse.Result, nil
+}
+
+// UpdateFilters updates many filters at once.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-multiple-filters
+func (api *API) UpdateFilters(zoneID string, filters []Filter) ([]Filter, error) {
+	for _, filter := range filters {
+		if filter.ID == "" {
+			return []Filter{}, errors.Errorf("filter ID cannot be empty")
+		}
+	}
+
+	uri := "/zones/" + zoneID + "/filters"
+
+	res, err := api.makeRequest("PUT", uri, filters)
+	if err != nil {
+		return []Filter{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var filtersResponse FiltersDetailResponse
+	err = json.Unmarshal(res, &filtersResponse)
+	if err != nil {
+		return []Filter{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return filtersResponse.Result, nil
+}
+
+// DeleteFilter deletes a single filter.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-a-single-filter
+func (api *API) DeleteFilter(zoneID, filterID string) error {
+	if filterID == "" {
+		return errors.Errorf("filter ID cannot be empty")
+	}
+
+	uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filterID)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
+
+// DeleteFilters deletes multiple filters.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-multiple-filters
+func (api *API) DeleteFilters(zoneID string, filterIDs []string) error {
+	ids := strings.Join(filterIDs, ",")
+	uri := fmt.Sprintf("/zones/%s/filters?id=%s", zoneID, ids)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
+
+// ValidateFilterExpression checks correctness of a filter expression.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/validation/
+func (api *API) ValidateFilterExpression(expression string) error {
+	uri := fmt.Sprintf("/filters/validate-expr")
+	expressionPayload := FilterValidateExpression{Expression: expression}
+
+	_, err := api.makeRequest("POST", uri, expressionPayload)
+	if err != nil {
+		var filterValidationResponse FilterValidateExpressionResponse
+
+		jsonErr := json.Unmarshal([]byte(err.Error()), &filterValidationResponse)
+		if jsonErr != nil {
+			return errors.Wrap(jsonErr, errUnmarshalError)
+		}
+
+		if filterValidationResponse.Success != true {
+			// Unsure why but the API returns `errors` as an array but it only
+			// ever shows the issue with one problem at a time ¯\_(ツ)_/¯
+			return errors.Errorf(filterValidationResponse.Errors[0].Message)
+		}
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/firewall.go b/vendor/github.com/cloudflare/cloudflare-go/firewall.go
new file mode 100644
index 000000000..4b61a7ca5
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/firewall.go
@@ -0,0 +1,280 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessRule represents a firewall access rule.
+type AccessRule struct {
+	ID            string                  `json:"id,omitempty"`
+	Notes         string                  `json:"notes,omitempty"`
+	AllowedModes  []string                `json:"allowed_modes,omitempty"`
+	Mode          string                  `json:"mode,omitempty"`
+	Configuration AccessRuleConfiguration `json:"configuration,omitempty"`
+	Scope         AccessRuleScope         `json:"scope,omitempty"`
+	CreatedOn     time.Time               `json:"created_on,omitempty"`
+	ModifiedOn    time.Time               `json:"modified_on,omitempty"`
+}
+
+// AccessRuleConfiguration represents the configuration of a firewall
+// access rule.
+type AccessRuleConfiguration struct {
+	Target string `json:"target,omitempty"`
+	Value  string `json:"value,omitempty"`
+}
+
+// AccessRuleScope represents the scope of a firewall access rule.
+type AccessRuleScope struct {
+	ID    string `json:"id,omitempty"`
+	Email string `json:"email,omitempty"`
+	Name  string `json:"name,omitempty"`
+	Type  string `json:"type,omitempty"`
+}
+
+// AccessRuleResponse represents the response from the firewall access
+// rule endpoint.
+type AccessRuleResponse struct {
+	Result AccessRule `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// AccessRuleListResponse represents the response from the list access rules
+// endpoint.
+type AccessRuleListResponse struct {
+	Result []AccessRule `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// ListUserAccessRules returns a slice of access rules for the logged-in user.
+//
+// This takes an AccessRule to allow filtering of the results returned.
+//
+// API reference: https://api.cloudflare.com/#user-level-firewall-access-rule-list-access-rules
+func (api *API) ListUserAccessRules(accessRule AccessRule, page int) (*AccessRuleListResponse, error) {
+	return api.listAccessRules("/user", accessRule, page)
+}
+
+// CreateUserAccessRule creates a firewall access rule for the logged-in user.
+//
+// API reference: https://api.cloudflare.com/#user-level-firewall-access-rule-create-access-rule
+func (api *API) CreateUserAccessRule(accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.createAccessRule("/user", accessRule)
+}
+
+// UserAccessRule returns the details of a user's account access rule.
+//
+// API reference: https://api.cloudflare.com/#user-level-firewall-access-rule-list-access-rules
+func (api *API) UserAccessRule(accessRuleID string) (*AccessRuleResponse, error) {
+	return api.retrieveAccessRule("/user", accessRuleID)
+}
+
+// UpdateUserAccessRule updates a single access rule for the logged-in user &
+// given access rule identifier.
+//
+// API reference: https://api.cloudflare.com/#user-level-firewall-access-rule-update-access-rule
+func (api *API) UpdateUserAccessRule(accessRuleID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.updateAccessRule("/user", accessRuleID, accessRule)
+}
+
+// DeleteUserAccessRule deletes a single access rule for the logged-in user and
+// access rule identifiers.
+//
+// API reference: https://api.cloudflare.com/#user-level-firewall-access-rule-update-access-rule
+func (api *API) DeleteUserAccessRule(accessRuleID string) (*AccessRuleResponse, error) {
+	return api.deleteAccessRule("/user", accessRuleID)
+}
+
+// ListZoneAccessRules returns a slice of access rules for the given zone
+// identifier.
+//
+// This takes an AccessRule to allow filtering of the results returned.
+//
+// API reference: https://api.cloudflare.com/#firewall-access-rule-for-a-zone-list-access-rules
+func (api *API) ListZoneAccessRules(zoneID string, accessRule AccessRule, page int) (*AccessRuleListResponse, error) {
+	return api.listAccessRules("/zones/"+zoneID, accessRule, page)
+}
+
+// CreateZoneAccessRule creates a firewall access rule for the given zone
+// identifier.
+//
+// API reference: https://api.cloudflare.com/#firewall-access-rule-for-a-zone-create-access-rule
+func (api *API) CreateZoneAccessRule(zoneID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.createAccessRule("/zones/"+zoneID, accessRule)
+}
+
+// ZoneAccessRule returns the details of a zone's access rule.
+//
+// API reference: https://api.cloudflare.com/#firewall-access-rule-for-a-zone-list-access-rules
+func (api *API) ZoneAccessRule(zoneID string, accessRuleID string) (*AccessRuleResponse, error) {
+	return api.retrieveAccessRule("/zones/"+zoneID, accessRuleID)
+}
+
+// UpdateZoneAccessRule updates a single access rule for the given zone &
+// access rule identifiers.
+//
+// API reference: https://api.cloudflare.com/#firewall-access-rule-for-a-zone-update-access-rule
+func (api *API) UpdateZoneAccessRule(zoneID, accessRuleID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.updateAccessRule("/zones/"+zoneID, accessRuleID, accessRule)
+}
+
+// DeleteZoneAccessRule deletes a single access rule for the given zone and
+// access rule identifiers.
+//
+// API reference: https://api.cloudflare.com/#firewall-access-rule-for-a-zone-delete-access-rule
+func (api *API) DeleteZoneAccessRule(zoneID, accessRuleID string) (*AccessRuleResponse, error) {
+	return api.deleteAccessRule("/zones/"+zoneID, accessRuleID)
+}
+
+// ListAccountAccessRules returns a slice of access rules for the given
+// account identifier.
+//
+// This takes an AccessRule to allow filtering of the results returned.
+//
+// API reference: https://api.cloudflare.com/#account-level-firewall-access-rule-list-access-rules
+func (api *API) ListAccountAccessRules(accountID string, accessRule AccessRule, page int) (*AccessRuleListResponse, error) {
+	return api.listAccessRules("/accounts/"+accountID, accessRule, page)
+}
+
+// CreateAccountAccessRule creates a firewall access rule for the given
+// account identifier.
+//
+// API reference: https://api.cloudflare.com/#account-level-firewall-access-rule-create-access-rule
+func (api *API) CreateAccountAccessRule(accountID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.createAccessRule("/accounts/"+accountID, accessRule)
+}
+
+// AccountAccessRule returns the details of an account's access rule.
+//
+// API reference: https://api.cloudflare.com/#account-level-firewall-access-rule-access-rule-details
+func (api *API) AccountAccessRule(accountID string, accessRuleID string) (*AccessRuleResponse, error) {
+	return api.retrieveAccessRule("/accounts/"+accountID, accessRuleID)
+}
+
+// UpdateAccountAccessRule updates a single access rule for the given
+// account & access rule identifiers.
+//
+// API reference: https://api.cloudflare.com/#account-level-firewall-access-rule-update-access-rule
+func (api *API) UpdateAccountAccessRule(accountID, accessRuleID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.updateAccessRule("/accounts/"+accountID, accessRuleID, accessRule)
+}
+
+// DeleteAccountAccessRule deletes a single access rule for the given
+// account and access rule identifiers.
+//
+// API reference: https://api.cloudflare.com/#account-level-firewall-access-rule-delete-access-rule
+func (api *API) DeleteAccountAccessRule(accountID, accessRuleID string) (*AccessRuleResponse, error) {
+	return api.deleteAccessRule("/accounts/"+accountID, accessRuleID)
+}
+
+func (api *API) listAccessRules(prefix string, accessRule AccessRule, page int) (*AccessRuleListResponse, error) {
+	// Construct a query string
+	v := url.Values{}
+	if page <= 0 {
+		page = 1
+	}
+	v.Set("page", strconv.Itoa(page))
+	// Request as many rules as possible per page - API max is 100
+	v.Set("per_page", "100")
+	if accessRule.Notes != "" {
+		v.Set("notes", accessRule.Notes)
+	}
+	if accessRule.Mode != "" {
+		v.Set("mode", accessRule.Mode)
+	}
+	if accessRule.Scope.Type != "" {
+		v.Set("scope_type", accessRule.Scope.Type)
+	}
+	if accessRule.Configuration.Value != "" {
+		v.Set("configuration_value", accessRule.Configuration.Value)
+	}
+	if accessRule.Configuration.Target != "" {
+		v.Set("configuration_target", accessRule.Configuration.Target)
+	}
+	v.Set("page", strconv.Itoa(page))
+	query := "?" + v.Encode()
+
+	uri := prefix + "/firewall/access_rules/rules" + query
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &AccessRuleListResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return response, nil
+}
+
+func (api *API) createAccessRule(prefix string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	uri := prefix + "/firewall/access_rules/rules"
+	res, err := api.makeRequest("POST", uri, accessRule)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &AccessRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+func (api *API) retrieveAccessRule(prefix, accessRuleID string) (*AccessRuleResponse, error) {
+	uri := prefix + "/firewall/access_rules/rules/" + accessRuleID
+
+	res, err := api.makeRequest("GET", uri, nil)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &AccessRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+func (api *API) updateAccessRule(prefix, accessRuleID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	uri := prefix + "/firewall/access_rules/rules/" + accessRuleID
+	res, err := api.makeRequest("PATCH", uri, accessRule)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &AccessRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return response, nil
+}
+
+func (api *API) deleteAccessRule(prefix, accessRuleID string) (*AccessRuleResponse, error) {
+	uri := prefix + "/firewall/access_rules/rules/" + accessRuleID
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &AccessRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/firewall_rules.go b/vendor/github.com/cloudflare/cloudflare-go/firewall_rules.go
new file mode 100644
index 000000000..7a6ce5c70
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/firewall_rules.go
@@ -0,0 +1,196 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// FirewallRule is the struct of the firewall rule.
+type FirewallRule struct {
+	ID          string      `json:"id,omitempty"`
+	Paused      bool        `json:"paused"`
+	Description string      `json:"description"`
+	Action      string      `json:"action"`
+	Priority    interface{} `json:"priority"`
+	Filter      Filter      `json:"filter"`
+	CreatedOn   time.Time   `json:"created_on,omitempty"`
+	ModifiedOn  time.Time   `json:"modified_on,omitempty"`
+}
+
+// FirewallRulesDetailResponse is the API response for the firewall
+// rules.
+type FirewallRulesDetailResponse struct {
+	Result     []FirewallRule `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+// FirewallRuleResponse is the API response that is returned
+// for requesting a single firewall rule on a zone.
+type FirewallRuleResponse struct {
+	Result     FirewallRule `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+// FirewallRules returns all firewall rules.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/get/#get-all-rules
+func (api *API) FirewallRules(zoneID string, pageOpts PaginationOptions) ([]FirewallRule, error) {
+	uri := fmt.Sprintf("/zones/%s/firewall/rules", zoneID)
+	v := url.Values{}
+
+	if pageOpts.PerPage > 0 {
+		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
+	}
+
+	if pageOpts.Page > 0 {
+		v.Set("page", strconv.Itoa(pageOpts.Page))
+	}
+
+	if len(v) > 0 {
+		uri = uri + "?" + v.Encode()
+	}
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []FirewallRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var firewallDetailResponse FirewallRulesDetailResponse
+	err = json.Unmarshal(res, &firewallDetailResponse)
+	if err != nil {
+		return []FirewallRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return firewallDetailResponse.Result, nil
+}
+
+// FirewallRule returns a single firewall rule based on the ID.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/get/#get-by-rule-id
+func (api *API) FirewallRule(zoneID, firewallRuleID string) (FirewallRule, error) {
+	uri := fmt.Sprintf("/zones/%s/firewall/rules/%s", zoneID, firewallRuleID)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return FirewallRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var firewallRuleResponse FirewallRuleResponse
+	err = json.Unmarshal(res, &firewallRuleResponse)
+	if err != nil {
+		return FirewallRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return firewallRuleResponse.Result, nil
+}
+
+// CreateFirewallRules creates new firewall rules.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/post/
+func (api *API) CreateFirewallRules(zoneID string, firewallRules []FirewallRule) ([]FirewallRule, error) {
+	uri := fmt.Sprintf("/zones/%s/firewall/rules", zoneID)
+
+	res, err := api.makeRequest("POST", uri, firewallRules)
+	if err != nil {
+		return []FirewallRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var firewallRulesDetailResponse FirewallRulesDetailResponse
+	err = json.Unmarshal(res, &firewallRulesDetailResponse)
+	if err != nil {
+		return []FirewallRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return firewallRulesDetailResponse.Result, nil
+}
+
+// UpdateFirewallRule updates a single firewall rule.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/put/#update-a-single-rule
+func (api *API) UpdateFirewallRule(zoneID string, firewallRule FirewallRule) (FirewallRule, error) {
+	if firewallRule.ID == "" {
+		return FirewallRule{}, errors.Errorf("firewall rule ID cannot be empty")
+	}
+
+	uri := fmt.Sprintf("/zones/%s/firewall/rules/%s", zoneID, firewallRule.ID)
+
+	res, err := api.makeRequest("PUT", uri, firewallRule)
+	if err != nil {
+		return FirewallRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var firewallRuleResponse FirewallRuleResponse
+	err = json.Unmarshal(res, &firewallRuleResponse)
+	if err != nil {
+		return FirewallRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return firewallRuleResponse.Result, nil
+}
+
+// UpdateFirewallRules updates a single firewall rule.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/put/#update-multiple-rules
+func (api *API) UpdateFirewallRules(zoneID string, firewallRules []FirewallRule) ([]FirewallRule, error) {
+	for _, firewallRule := range firewallRules {
+		if firewallRule.ID == "" {
+			return []FirewallRule{}, errors.Errorf("firewall ID cannot be empty")
+		}
+	}
+
+	uri := fmt.Sprintf("/zones/%s/firewall/rules", zoneID)
+
+	res, err := api.makeRequest("PUT", uri, firewallRules)
+	if err != nil {
+		return []FirewallRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var firewallRulesDetailResponse FirewallRulesDetailResponse
+	err = json.Unmarshal(res, &firewallRulesDetailResponse)
+	if err != nil {
+		return []FirewallRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return firewallRulesDetailResponse.Result, nil
+}
+
+// DeleteFirewallRule updates a single firewall rule.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/delete/#delete-a-single-rule
+func (api *API) DeleteFirewallRule(zoneID, firewallRuleID string) error {
+	if firewallRuleID == "" {
+		return errors.Errorf("firewall rule ID cannot be empty")
+	}
+
+	uri := fmt.Sprintf("/zones/%s/firewall/rules/%s", zoneID, firewallRuleID)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
+
+// DeleteFirewallRules updates a single firewall rule.
+//
+// API reference: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/delete/#delete-multiple-rules
+func (api *API) DeleteFirewallRules(zoneID string, firewallRuleIDs []string) error {
+	ids := strings.Join(firewallRuleIDs, ",")
+	uri := fmt.Sprintf("/zones/%s/firewall/rules?id=%s", zoneID, ids)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/go.mod b/vendor/github.com/cloudflare/cloudflare-go/go.mod
new file mode 100644
index 000000000..77e922338
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/go.mod
@@ -0,0 +1,13 @@
+module github.com/cloudflare/cloudflare-go
+
+go 1.11
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/mattn/go-runewidth v0.0.4 // indirect
+	github.com/olekukonko/tablewriter v0.0.1
+	github.com/pkg/errors v0.8.1
+	github.com/stretchr/testify v1.4.0
+	github.com/urfave/cli v1.22.1
+	golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
+)
diff --git a/vendor/github.com/cloudflare/cloudflare-go/go.sum b/vendor/github.com/cloudflare/cloudflare-go/go.sum
new file mode 100644
index 000000000..65391c2b1
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/go.sum
@@ -0,0 +1,26 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
+github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE=
+github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/cloudflare/cloudflare-go/ips.go b/vendor/github.com/cloudflare/cloudflare-go/ips.go
new file mode 100644
index 000000000..72b5fcfbc
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/ips.go
@@ -0,0 +1,44 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/pkg/errors"
+)
+
+// IPRanges contains lists of IPv4 and IPv6 CIDRs.
+type IPRanges struct {
+	IPv4CIDRs []string `json:"ipv4_cidrs"`
+	IPv6CIDRs []string `json:"ipv6_cidrs"`
+}
+
+// IPsResponse is the API response containing a list of IPs.
+type IPsResponse struct {
+	Response
+	Result IPRanges `json:"result"`
+}
+
+// IPs gets a list of Cloudflare's IP ranges.
+//
+// This does not require logging in to the API.
+//
+// API reference: https://api.cloudflare.com/#cloudflare-ips
+func IPs() (IPRanges, error) {
+	resp, err := http.Get(apiURL + "/ips")
+	if err != nil {
+		return IPRanges{}, errors.Wrap(err, "HTTP request failed")
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return IPRanges{}, errors.Wrap(err, "Response body could not be read")
+	}
+	var r IPsResponse
+	err = json.Unmarshal(body, &r)
+	if err != nil {
+		return IPRanges{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/keyless.go b/vendor/github.com/cloudflare/cloudflare-go/keyless.go
new file mode 100644
index 000000000..c5cc83914
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/keyless.go
@@ -0,0 +1,52 @@
+package cloudflare
+
+import "time"
+
+// KeylessSSL represents Keyless SSL configuration.
+type KeylessSSL struct {
+	ID          string    `json:"id"`
+	Name        string    `json:"name"`
+	Host        string    `json:"host"`
+	Port        int       `json:"port"`
+	Status      string    `json:"success"`
+	Enabled     bool      `json:"enabled"`
+	Permissions []string  `json:"permissions"`
+	CreatedOn   time.Time `json:"created_on"`
+	ModifiedOn  time.Time `json:"modifed_on"`
+}
+
+// KeylessSSLResponse represents the response from the Keyless SSL endpoint.
+type KeylessSSLResponse struct {
+	Response
+	Result []KeylessSSL `json:"result"`
+}
+
+// CreateKeyless creates a new Keyless SSL configuration for the zone.
+//
+// API reference: https://api.cloudflare.com/#keyless-ssl-for-a-zone-create-a-keyless-ssl-configuration
+func (api *API) CreateKeyless() {
+}
+
+// ListKeyless lists Keyless SSL configurations for a zone.
+//
+// API reference: https://api.cloudflare.com/#keyless-ssl-for-a-zone-list-keyless-ssls
+func (api *API) ListKeyless() {
+}
+
+// Keyless provides the configuration for a given Keyless SSL identifier.
+//
+// API reference: https://api.cloudflare.com/#keyless-ssl-for-a-zone-keyless-ssl-details
+func (api *API) Keyless() {
+}
+
+// UpdateKeyless updates an existing Keyless SSL configuration.
+//
+// API reference: https://api.cloudflare.com/#keyless-ssl-for-a-zone-update-keyless-configuration
+func (api *API) UpdateKeyless() {
+}
+
+// DeleteKeyless deletes an existing Keyless SSL configuration.
+//
+// API reference: https://api.cloudflare.com/#keyless-ssl-for-a-zone-delete-keyless-configuration
+func (api *API) DeleteKeyless() {
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/load_balancing.go b/vendor/github.com/cloudflare/cloudflare-go/load_balancing.go
new file mode 100644
index 000000000..8b2f89a65
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/load_balancing.go
@@ -0,0 +1,387 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// LoadBalancerPool represents a load balancer pool's properties.
+type LoadBalancerPool struct {
+	ID                string               `json:"id,omitempty"`
+	CreatedOn         *time.Time           `json:"created_on,omitempty"`
+	ModifiedOn        *time.Time           `json:"modified_on,omitempty"`
+	Description       string               `json:"description"`
+	Name              string               `json:"name"`
+	Enabled           bool                 `json:"enabled"`
+	MinimumOrigins    int                  `json:"minimum_origins,omitempty"`
+	Monitor           string               `json:"monitor,omitempty"`
+	Origins           []LoadBalancerOrigin `json:"origins"`
+	NotificationEmail string               `json:"notification_email,omitempty"`
+
+	// CheckRegions defines the geographic region(s) from where to run health-checks from - e.g. "WNAM", "WEU", "SAF", "SAM".
+	// Providing a null/empty value means "all regions", which may not be available to all plan types.
+	CheckRegions []string `json:"check_regions"`
+}
+
+// LoadBalancerOrigin represents a Load Balancer origin's properties.
+type LoadBalancerOrigin struct {
+	Name    string  `json:"name"`
+	Address string  `json:"address"`
+	Enabled bool    `json:"enabled"`
+	Weight  float64 `json:"weight"`
+}
+
+// LoadBalancerMonitor represents a load balancer monitor's properties.
+type LoadBalancerMonitor struct {
+	ID              string              `json:"id,omitempty"`
+	CreatedOn       *time.Time          `json:"created_on,omitempty"`
+	ModifiedOn      *time.Time          `json:"modified_on,omitempty"`
+	Type            string              `json:"type"`
+	Description     string              `json:"description"`
+	Method          string              `json:"method"`
+	Path            string              `json:"path"`
+	Header          map[string][]string `json:"header"`
+	Timeout         int                 `json:"timeout"`
+	Retries         int                 `json:"retries"`
+	Interval        int                 `json:"interval"`
+	Port            uint16              `json:"port,omitempty"`
+	ExpectedBody    string              `json:"expected_body"`
+	ExpectedCodes   string              `json:"expected_codes"`
+	FollowRedirects bool                `json:"follow_redirects"`
+	AllowInsecure   bool                `json:"allow_insecure"`
+	ProbeZone       string              `json:"probe_zone"`
+}
+
+// LoadBalancer represents a load balancer's properties.
+type LoadBalancer struct {
+	ID             string              `json:"id,omitempty"`
+	CreatedOn      *time.Time          `json:"created_on,omitempty"`
+	ModifiedOn     *time.Time          `json:"modified_on,omitempty"`
+	Description    string              `json:"description"`
+	Name           string              `json:"name"`
+	TTL            int                 `json:"ttl,omitempty"`
+	FallbackPool   string              `json:"fallback_pool"`
+	DefaultPools   []string            `json:"default_pools"`
+	RegionPools    map[string][]string `json:"region_pools"`
+	PopPools       map[string][]string `json:"pop_pools"`
+	Proxied        bool                `json:"proxied"`
+	Enabled        *bool               `json:"enabled,omitempty"`
+	Persistence    string              `json:"session_affinity,omitempty"`
+	PersistenceTTL int                 `json:"session_affinity_ttl,omitempty"`
+
+	// SteeringPolicy controls pool selection logic.
+	// "off" select pools in DefaultPools order
+	// "geo" select pools based on RegionPools/PopPools
+	// "dynamic_latency" select pools based on RTT (requires health checks)
+	// "random" selects pools in a random order
+	// "" maps to "geo" if RegionPools or PopPools have entries otherwise "off"
+	SteeringPolicy string `json:"steering_policy,omitempty"`
+}
+
+// LoadBalancerOriginHealth represents the health of the origin.
+type LoadBalancerOriginHealth struct {
+	Healthy       bool     `json:"healthy,omitempty"`
+	RTT           Duration `json:"rtt,omitempty"`
+	FailureReason string   `json:"failure_reason,omitempty"`
+	ResponseCode  int      `json:"response_code,omitempty"`
+}
+
+// LoadBalancerPoolPopHealth represents the health of the pool for given PoP.
+type LoadBalancerPoolPopHealth struct {
+	Healthy bool                                  `json:"healthy,omitempty"`
+	Origins []map[string]LoadBalancerOriginHealth `json:"origins,omitempty"`
+}
+
+// LoadBalancerPoolHealth represents the healthchecks from different PoPs for a pool.
+type LoadBalancerPoolHealth struct {
+	ID        string                               `json:"pool_id,omitempty"`
+	PopHealth map[string]LoadBalancerPoolPopHealth `json:"pop_health,omitempty"`
+}
+
+// loadBalancerPoolResponse represents the response from the load balancer pool endpoints.
+type loadBalancerPoolResponse struct {
+	Response
+	Result LoadBalancerPool `json:"result"`
+}
+
+// loadBalancerPoolListResponse represents the response from the List Pools endpoint.
+type loadBalancerPoolListResponse struct {
+	Response
+	Result     []LoadBalancerPool `json:"result"`
+	ResultInfo ResultInfo         `json:"result_info"`
+}
+
+// loadBalancerMonitorResponse represents the response from the load balancer monitor endpoints.
+type loadBalancerMonitorResponse struct {
+	Response
+	Result LoadBalancerMonitor `json:"result"`
+}
+
+// loadBalancerMonitorListResponse represents the response from the List Monitors endpoint.
+type loadBalancerMonitorListResponse struct {
+	Response
+	Result     []LoadBalancerMonitor `json:"result"`
+	ResultInfo ResultInfo            `json:"result_info"`
+}
+
+// loadBalancerResponse represents the response from the load balancer endpoints.
+type loadBalancerResponse struct {
+	Response
+	Result LoadBalancer `json:"result"`
+}
+
+// loadBalancerListResponse represents the response from the List Load Balancers endpoint.
+type loadBalancerListResponse struct {
+	Response
+	Result     []LoadBalancer `json:"result"`
+	ResultInfo ResultInfo     `json:"result_info"`
+}
+
+// loadBalancerPoolHealthResponse represents the response from the Pool Health Details endpoint.
+type loadBalancerPoolHealthResponse struct {
+	Response
+	Result LoadBalancerPoolHealth `json:"result"`
+}
+
+// CreateLoadBalancerPool creates a new load balancer pool.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-pools-create-a-pool
+func (api *API) CreateLoadBalancerPool(pool LoadBalancerPool) (LoadBalancerPool, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/pools"
+	res, err := api.makeRequest("POST", uri, pool)
+	if err != nil {
+		return LoadBalancerPool{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerPoolResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancerPool{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListLoadBalancerPools lists load balancer pools connected to an account.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-pools-list-pools
+func (api *API) ListLoadBalancerPools() ([]LoadBalancerPool, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/pools"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerPoolListResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// LoadBalancerPoolDetails returns the details for a load balancer pool.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-pools-pool-details
+func (api *API) LoadBalancerPoolDetails(poolID string) (LoadBalancerPool, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/pools/" + poolID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return LoadBalancerPool{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerPoolResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancerPool{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// DeleteLoadBalancerPool disables and deletes a load balancer pool.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-pools-delete-a-pool
+func (api *API) DeleteLoadBalancerPool(poolID string) error {
+	uri := api.userBaseURL("/user") + "/load_balancers/pools/" + poolID
+	if _, err := api.makeRequest("DELETE", uri, nil); err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	return nil
+}
+
+// ModifyLoadBalancerPool modifies a configured load balancer pool.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-pools-modify-a-pool
+func (api *API) ModifyLoadBalancerPool(pool LoadBalancerPool) (LoadBalancerPool, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/pools/" + pool.ID
+	res, err := api.makeRequest("PUT", uri, pool)
+	if err != nil {
+		return LoadBalancerPool{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerPoolResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancerPool{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// CreateLoadBalancerMonitor creates a new load balancer monitor.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-monitors-create-a-monitor
+func (api *API) CreateLoadBalancerMonitor(monitor LoadBalancerMonitor) (LoadBalancerMonitor, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/monitors"
+	res, err := api.makeRequest("POST", uri, monitor)
+	if err != nil {
+		return LoadBalancerMonitor{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerMonitorResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancerMonitor{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListLoadBalancerMonitors lists load balancer monitors connected to an account.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-monitors-list-monitors
+func (api *API) ListLoadBalancerMonitors() ([]LoadBalancerMonitor, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/monitors"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerMonitorListResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// LoadBalancerMonitorDetails returns the details for a load balancer monitor.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-monitors-monitor-details
+func (api *API) LoadBalancerMonitorDetails(monitorID string) (LoadBalancerMonitor, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/monitors/" + monitorID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return LoadBalancerMonitor{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerMonitorResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancerMonitor{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// DeleteLoadBalancerMonitor disables and deletes a load balancer monitor.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-monitors-delete-a-monitor
+func (api *API) DeleteLoadBalancerMonitor(monitorID string) error {
+	uri := api.userBaseURL("/user") + "/load_balancers/monitors/" + monitorID
+	if _, err := api.makeRequest("DELETE", uri, nil); err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	return nil
+}
+
+// ModifyLoadBalancerMonitor modifies a configured load balancer monitor.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-monitors-modify-a-monitor
+func (api *API) ModifyLoadBalancerMonitor(monitor LoadBalancerMonitor) (LoadBalancerMonitor, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/monitors/" + monitor.ID
+	res, err := api.makeRequest("PUT", uri, monitor)
+	if err != nil {
+		return LoadBalancerMonitor{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerMonitorResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancerMonitor{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// CreateLoadBalancer creates a new load balancer.
+//
+// API reference: https://api.cloudflare.com/#load-balancers-create-a-load-balancer
+func (api *API) CreateLoadBalancer(zoneID string, lb LoadBalancer) (LoadBalancer, error) {
+	uri := "/zones/" + zoneID + "/load_balancers"
+	res, err := api.makeRequest("POST", uri, lb)
+	if err != nil {
+		return LoadBalancer{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancer{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListLoadBalancers lists load balancers configured on a zone.
+//
+// API reference: https://api.cloudflare.com/#load-balancers-list-load-balancers
+func (api *API) ListLoadBalancers(zoneID string) ([]LoadBalancer, error) {
+	uri := "/zones/" + zoneID + "/load_balancers"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerListResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// LoadBalancerDetails returns the details for a load balancer.
+//
+// API reference: https://api.cloudflare.com/#load-balancers-load-balancer-details
+func (api *API) LoadBalancerDetails(zoneID, lbID string) (LoadBalancer, error) {
+	uri := "/zones/" + zoneID + "/load_balancers/" + lbID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return LoadBalancer{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancer{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// DeleteLoadBalancer disables and deletes a load balancer.
+//
+// API reference: https://api.cloudflare.com/#load-balancers-delete-a-load-balancer
+func (api *API) DeleteLoadBalancer(zoneID, lbID string) error {
+	uri := "/zones/" + zoneID + "/load_balancers/" + lbID
+	if _, err := api.makeRequest("DELETE", uri, nil); err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	return nil
+}
+
+// ModifyLoadBalancer modifies a configured load balancer.
+//
+// API reference: https://api.cloudflare.com/#load-balancers-modify-a-load-balancer
+func (api *API) ModifyLoadBalancer(zoneID string, lb LoadBalancer) (LoadBalancer, error) {
+	uri := "/zones/" + zoneID + "/load_balancers/" + lb.ID
+	res, err := api.makeRequest("PUT", uri, lb)
+	if err != nil {
+		return LoadBalancer{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancer{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// PoolHealthDetails fetches the latest healtcheck details for a single pool.
+//
+// API reference: https://api.cloudflare.com/#load-balancer-pools-pool-health-details
+func (api *API) PoolHealthDetails(poolID string) (LoadBalancerPoolHealth, error) {
+	uri := api.userBaseURL("/user") + "/load_balancers/pools/" + poolID + "/health"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return LoadBalancerPoolHealth{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r loadBalancerPoolHealthResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return LoadBalancerPoolHealth{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/lockdown.go b/vendor/github.com/cloudflare/cloudflare-go/lockdown.go
new file mode 100644
index 000000000..164129bc5
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/lockdown.go
@@ -0,0 +1,151 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+// ZoneLockdown represents a Zone Lockdown rule. A rule only permits access to
+// the provided URL pattern(s) from the given IP address(es) or subnet(s).
+type ZoneLockdown struct {
+	ID             string               `json:"id"`
+	Description    string               `json:"description"`
+	URLs           []string             `json:"urls"`
+	Configurations []ZoneLockdownConfig `json:"configurations"`
+	Paused         bool                 `json:"paused"`
+	Priority       int                  `json:"priority,omitempty"`
+}
+
+// ZoneLockdownConfig represents a Zone Lockdown config, which comprises
+// a Target ("ip" or "ip_range") and a Value (an IP address or IP+mask,
+// respectively.)
+type ZoneLockdownConfig struct {
+	Target string `json:"target"`
+	Value  string `json:"value"`
+}
+
+// ZoneLockdownResponse represents a response from the Zone Lockdown endpoint.
+type ZoneLockdownResponse struct {
+	Result ZoneLockdown `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// ZoneLockdownListResponse represents a response from the List Zone Lockdown
+// endpoint.
+type ZoneLockdownListResponse struct {
+	Result []ZoneLockdown `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// CreateZoneLockdown creates a Zone ZoneLockdown rule for the given zone ID.
+//
+// API reference: https://api.cloudflare.com/#zone-ZoneLockdown-create-a-ZoneLockdown-rule
+func (api *API) CreateZoneLockdown(zoneID string, ld ZoneLockdown) (*ZoneLockdownResponse, error) {
+	uri := "/zones/" + zoneID + "/firewall/lockdowns"
+	res, err := api.makeRequest("POST", uri, ld)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &ZoneLockdownResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// UpdateZoneLockdown updates a Zone ZoneLockdown rule (based on the ID) for the
+// given zone ID.
+//
+// API reference: https://api.cloudflare.com/#zone-ZoneLockdown-update-ZoneLockdown-rule
+func (api *API) UpdateZoneLockdown(zoneID string, id string, ld ZoneLockdown) (*ZoneLockdownResponse, error) {
+	uri := "/zones/" + zoneID + "/firewall/lockdowns/" + id
+	res, err := api.makeRequest("PUT", uri, ld)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &ZoneLockdownResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// DeleteZoneLockdown deletes a Zone ZoneLockdown rule (based on the ID) for the
+// given zone ID.
+//
+// API reference: https://api.cloudflare.com/#zone-ZoneLockdown-delete-ZoneLockdown-rule
+func (api *API) DeleteZoneLockdown(zoneID string, id string) (*ZoneLockdownResponse, error) {
+	uri := "/zones/" + zoneID + "/firewall/lockdowns/" + id
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &ZoneLockdownResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// ZoneLockdown retrieves a Zone ZoneLockdown rule (based on the ID) for the
+// given zone ID.
+//
+// API reference: https://api.cloudflare.com/#zone-ZoneLockdown-ZoneLockdown-rule-details
+func (api *API) ZoneLockdown(zoneID string, id string) (*ZoneLockdownResponse, error) {
+	uri := "/zones/" + zoneID + "/firewall/lockdowns/" + id
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &ZoneLockdownResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// ListZoneLockdowns retrieves a list of Zone ZoneLockdown rules for a given
+// zone ID by page number.
+//
+// API reference: https://api.cloudflare.com/#zone-ZoneLockdown-list-ZoneLockdown-rules
+func (api *API) ListZoneLockdowns(zoneID string, page int) (*ZoneLockdownListResponse, error) {
+	v := url.Values{}
+	if page <= 0 {
+		page = 1
+	}
+
+	v.Set("page", strconv.Itoa(page))
+	v.Set("per_page", strconv.Itoa(100))
+	query := "?" + v.Encode()
+
+	uri := "/zones/" + zoneID + "/firewall/lockdowns" + query
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &ZoneLockdownListResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/logpush.go b/vendor/github.com/cloudflare/cloudflare-go/logpush.go
new file mode 100644
index 000000000..a0134aded
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/logpush.go
@@ -0,0 +1,224 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"strconv"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// LogpushJob describes a Logpush job.
+type LogpushJob struct {
+	ID                 int        `json:"id,omitempty"`
+	Enabled            bool       `json:"enabled"`
+	Name               string     `json:"name"`
+	LogpullOptions     string     `json:"logpull_options"`
+	DestinationConf    string     `json:"destination_conf"`
+	OwnershipChallenge string     `json:"ownership_challenge,omitempty"`
+	LastComplete       *time.Time `json:"last_complete,omitempty"`
+	LastError          *time.Time `json:"last_error,omitempty"`
+	ErrorMessage       string     `json:"error_message,omitempty"`
+}
+
+// LogpushJobsResponse is the API response, containing an array of Logpush Jobs.
+type LogpushJobsResponse struct {
+	Response
+	Result []LogpushJob `json:"result"`
+}
+
+// LogpushJobDetailsResponse is the API response, containing a single Logpush Job.
+type LogpushJobDetailsResponse struct {
+	Response
+	Result LogpushJob `json:"result"`
+}
+
+// LogpushGetOwnershipChallenge describes a ownership validation.
+type LogpushGetOwnershipChallenge struct {
+	Filename string `json:"filename"`
+	Valid    bool   `json:"valid"`
+	Message  string `json:"message"`
+}
+
+// LogpushGetOwnershipChallengeResponse is the API response, containing a ownership challenge.
+type LogpushGetOwnershipChallengeResponse struct {
+	Response
+	Result LogpushGetOwnershipChallenge `json:"result"`
+}
+
+// LogpushGetOwnershipChallengeRequest is the API request for get ownership challenge.
+type LogpushGetOwnershipChallengeRequest struct {
+	DestinationConf string `json:"destination_conf"`
+}
+
+// LogpushOwnershipChallangeValidationResponse is the API response,
+// containing a ownership challenge validation result.
+type LogpushOwnershipChallangeValidationResponse struct {
+	Response
+	Result struct {
+		Valid bool `json:"valid"`
+	}
+}
+
+// LogpushValidateOwnershipChallengeRequest is the API request for validate ownership challenge.
+type LogpushValidateOwnershipChallengeRequest struct {
+	DestinationConf    string `json:"destination_conf"`
+	OwnershipChallenge string `json:"ownership_challenge"`
+}
+
+// LogpushDestinationExistsResponse is the API response,
+// containing a destination exists check result.
+type LogpushDestinationExistsResponse struct {
+	Response
+	Result struct {
+		Exists bool `json:"exists"`
+	}
+}
+
+// LogpushDestinationExistsRequest is the API request for check destination exists.
+type LogpushDestinationExistsRequest struct {
+	DestinationConf string `json:"destination_conf"`
+}
+
+// CreateLogpushJob creates a new LogpushJob for a zone.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-create-logpush-job
+func (api *API) CreateLogpushJob(zoneID string, job LogpushJob) (*LogpushJob, error) {
+	uri := "/zones/" + zoneID + "/logpush/jobs"
+	res, err := api.makeRequest("POST", uri, job)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushJobDetailsResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return &r.Result, nil
+}
+
+// LogpushJobs returns all Logpush Jobs for a zone.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-list-logpush-jobs
+func (api *API) LogpushJobs(zoneID string) ([]LogpushJob, error) {
+	uri := "/zones/" + zoneID + "/logpush/jobs"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []LogpushJob{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushJobsResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []LogpushJob{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// LogpushJob fetches detail about one Logpush Job for a zone.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-logpush-job-details
+func (api *API) LogpushJob(zoneID string, jobID int) (LogpushJob, error) {
+	uri := "/zones/" + zoneID + "/logpush/jobs/" + strconv.Itoa(jobID)
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return LogpushJob{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushJobDetailsResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return LogpushJob{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// UpdateLogpushJob lets you update a Logpush Job.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-update-logpush-job
+func (api *API) UpdateLogpushJob(zoneID string, jobID int, job LogpushJob) error {
+	uri := "/zones/" + zoneID + "/logpush/jobs/" + strconv.Itoa(jobID)
+	res, err := api.makeRequest("PUT", uri, job)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushJobDetailsResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
+
+// DeleteLogpushJob deletes a Logpush Job for a zone.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-delete-logpush-job
+func (api *API) DeleteLogpushJob(zoneID string, jobID int) error {
+	uri := "/zones/" + zoneID + "/logpush/jobs/" + strconv.Itoa(jobID)
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushJobDetailsResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
+
+// GetLogpushOwnershipChallenge returns ownership challenge.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-get-ownership-challenge
+func (api *API) GetLogpushOwnershipChallenge(zoneID, destinationConf string) (*LogpushGetOwnershipChallenge, error) {
+	uri := "/zones/" + zoneID + "/logpush/ownership"
+	res, err := api.makeRequest("POST", uri, LogpushGetOwnershipChallengeRequest{
+		DestinationConf: destinationConf,
+	})
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushGetOwnershipChallengeResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return &r.Result, nil
+}
+
+// ValidateLogpushOwnershipChallenge returns ownership challenge validation result.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-validate-ownership-challenge
+func (api *API) ValidateLogpushOwnershipChallenge(zoneID, destinationConf, ownershipChallenge string) (bool, error) {
+	uri := "/zones/" + zoneID + "/logpush/ownership/validate"
+	res, err := api.makeRequest("POST", uri, LogpushValidateOwnershipChallengeRequest{
+		DestinationConf:    destinationConf,
+		OwnershipChallenge: ownershipChallenge,
+	})
+	if err != nil {
+		return false, errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushGetOwnershipChallengeResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return false, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result.Valid, nil
+}
+
+// CheckLogpushDestinationExists returns destination exists check result.
+//
+// API reference: https://api.cloudflare.com/#logpush-jobs-check-destination-exists
+func (api *API) CheckLogpushDestinationExists(zoneID, destinationConf string) (bool, error) {
+	uri := "/zones/" + zoneID + "/logpush/validate/destination/exists"
+	res, err := api.makeRequest("POST", uri, LogpushDestinationExistsRequest{
+		DestinationConf: destinationConf,
+	})
+	if err != nil {
+		return false, errors.Wrap(err, errMakeRequestError)
+	}
+	var r LogpushDestinationExistsResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return false, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result.Exists, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/options.go b/vendor/github.com/cloudflare/cloudflare-go/options.go
new file mode 100644
index 000000000..1bf4f60bd
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/options.go
@@ -0,0 +1,101 @@
+package cloudflare
+
+import (
+	"net/http"
+
+	"time"
+
+	"golang.org/x/time/rate"
+)
+
+// Option is a functional option for configuring the API client.
+type Option func(*API) error
+
+// HTTPClient accepts a custom *http.Client for making API calls.
+func HTTPClient(client *http.Client) Option {
+	return func(api *API) error {
+		api.httpClient = client
+		return nil
+	}
+}
+
+// Headers allows you to set custom HTTP headers when making API calls (e.g. for
+// satisfying HTTP proxies, or for debugging).
+func Headers(headers http.Header) Option {
+	return func(api *API) error {
+		api.headers = headers
+		return nil
+	}
+}
+
+// UsingAccount allows you to apply account-level changes (Load Balancing,
+// Railguns) to an account instead.
+func UsingAccount(accountID string) Option {
+	return func(api *API) error {
+		api.AccountID = accountID
+		return nil
+	}
+}
+
+// UsingRateLimit applies a non-default rate limit to client API requests
+// If not specified the default of 4rps will be applied
+func UsingRateLimit(rps float64) Option {
+	return func(api *API) error {
+		// because ratelimiter doesnt do any windowing
+		// setting burst makes it difficult to enforce a fixed rate
+		// so setting it equal to 1 this effectively disables bursting
+		// this doesn't check for sensible values, ultimately the api will enforce that the value is ok
+		api.rateLimiter = rate.NewLimiter(rate.Limit(rps), 1)
+		return nil
+	}
+}
+
+// UsingRetryPolicy applies a non-default number of retries and min/max retry delays
+// This will be used when the client exponentially backs off after errored requests
+func UsingRetryPolicy(maxRetries int, minRetryDelaySecs int, maxRetryDelaySecs int) Option {
+	// seconds is very granular for a minimum delay - but this is only in case of failure
+	return func(api *API) error {
+		api.retryPolicy = RetryPolicy{
+			MaxRetries:    maxRetries,
+			MinRetryDelay: time.Duration(minRetryDelaySecs) * time.Second,
+			MaxRetryDelay: time.Duration(maxRetryDelaySecs) * time.Second,
+		}
+		return nil
+	}
+}
+
+// UsingLogger can be set if you want to get log output from this API instance
+// By default no log output is emitted
+func UsingLogger(logger Logger) Option {
+	return func(api *API) error {
+		api.logger = logger
+		return nil
+	}
+}
+
+// UserAgent can be set if you want to send a software name and version for HTTP access logs.
+// It is recommended to set it in order to help future Customer Support diagnostics
+// and prevent collateral damage by sharing generic User-Agent string with abusive users.
+// E.g. "my-software/1.2.3". By default generic Go User-Agent is used.
+func UserAgent(userAgent string) Option {
+	return func(api *API) error {
+		api.UserAgent = userAgent
+		return nil
+	}
+}
+
+// parseOptions parses the supplied options functions and returns a configured
+// *API instance.
+func (api *API) parseOptions(opts ...Option) error {
+	// Range over each options function and apply it to our API type to
+	// configure it. Options functions are applied in order, with any
+	// conflicting options overriding earlier calls.
+	for _, option := range opts {
+		err := option(api)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/origin_ca.go b/vendor/github.com/cloudflare/cloudflare-go/origin_ca.go
new file mode 100644
index 000000000..fdd8c4273
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/origin_ca.go
@@ -0,0 +1,169 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"net/url"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// OriginCACertificate represents a Cloudflare-issued certificate.
+//
+// API reference: https://api.cloudflare.com/#cloudflare-ca
+type OriginCACertificate struct {
+	ID              string    `json:"id"`
+	Certificate     string    `json:"certificate"`
+	Hostnames       []string  `json:"hostnames"`
+	ExpiresOn       time.Time `json:"expires_on"`
+	RequestType     string    `json:"request_type"`
+	RequestValidity int       `json:"requested_validity"`
+	CSR             string    `json:"csr"`
+}
+
+// OriginCACertificateListOptions represents the parameters used to list Cloudflare-issued certificates.
+type OriginCACertificateListOptions struct {
+	ZoneID string
+}
+
+// OriginCACertificateID represents the ID of the revoked certificate from the Revoke Certificate endpoint.
+type OriginCACertificateID struct {
+	ID string `json:"id"`
+}
+
+// originCACertificateResponse represents the response from the Create Certificate and the Certificate Details endpoints.
+type originCACertificateResponse struct {
+	Response
+	Result OriginCACertificate `json:"result"`
+}
+
+// originCACertificateResponseList represents the response from the List Certificates endpoint.
+type originCACertificateResponseList struct {
+	Response
+	Result     []OriginCACertificate `json:"result"`
+	ResultInfo ResultInfo            `json:"result_info"`
+}
+
+// originCACertificateResponseRevoke represents the response from the Revoke Certificate endpoint.
+type originCACertificateResponseRevoke struct {
+	Response
+	Result OriginCACertificateID `json:"result"`
+}
+
+// CreateOriginCertificate creates a Cloudflare-signed certificate.
+//
+// This function requires api.APIUserServiceKey be set to your Certificates API key.
+//
+// API reference: https://api.cloudflare.com/#cloudflare-ca-create-certificate
+func (api *API) CreateOriginCertificate(certificate OriginCACertificate) (*OriginCACertificate, error) {
+	uri := "/certificates"
+	res, err := api.makeRequestWithAuthType(context.TODO(), "POST", uri, certificate, AuthUserService)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var originResponse *originCACertificateResponse
+
+	err = json.Unmarshal(res, &originResponse)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	if !originResponse.Success {
+		return nil, errors.New(errRequestNotSuccessful)
+	}
+
+	return &originResponse.Result, nil
+}
+
+// OriginCertificates lists all Cloudflare-issued certificates.
+//
+// This function requires api.APIUserServiceKey be set to your Certificates API key.
+//
+// API reference: https://api.cloudflare.com/#cloudflare-ca-list-certificates
+func (api *API) OriginCertificates(options OriginCACertificateListOptions) ([]OriginCACertificate, error) {
+	v := url.Values{}
+	if options.ZoneID != "" {
+		v.Set("zone_id", options.ZoneID)
+	}
+	uri := "/certificates" + "?" + v.Encode()
+	res, err := api.makeRequestWithAuthType(context.TODO(), "GET", uri, nil, AuthUserService)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var originResponse *originCACertificateResponseList
+
+	err = json.Unmarshal(res, &originResponse)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	if !originResponse.Success {
+		return nil, errors.New(errRequestNotSuccessful)
+	}
+
+	return originResponse.Result, nil
+}
+
+// OriginCertificate returns the details for a Cloudflare-issued certificate.
+//
+// This function requires api.APIUserServiceKey be set to your Certificates API key.
+//
+// API reference: https://api.cloudflare.com/#cloudflare-ca-certificate-details
+func (api *API) OriginCertificate(certificateID string) (*OriginCACertificate, error) {
+	uri := "/certificates/" + certificateID
+	res, err := api.makeRequestWithAuthType(context.TODO(), "GET", uri, nil, AuthUserService)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var originResponse *originCACertificateResponse
+
+	err = json.Unmarshal(res, &originResponse)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	if !originResponse.Success {
+		return nil, errors.New(errRequestNotSuccessful)
+	}
+
+	return &originResponse.Result, nil
+}
+
+// RevokeOriginCertificate revokes a created certificate for a zone.
+//
+// This function requires api.APIUserServiceKey be set to your Certificates API key.
+//
+// API reference: https://api.cloudflare.com/#cloudflare-ca-revoke-certificate
+func (api *API) RevokeOriginCertificate(certificateID string) (*OriginCACertificateID, error) {
+	uri := "/certificates/" + certificateID
+	res, err := api.makeRequestWithAuthType(context.TODO(), "DELETE", uri, nil, AuthUserService)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var originResponse *originCACertificateResponseRevoke
+
+	err = json.Unmarshal(res, &originResponse)
+
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	if !originResponse.Success {
+		return nil, errors.New(errRequestNotSuccessful)
+	}
+
+	return &originResponse.Result, nil
+
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/page_rules.go b/vendor/github.com/cloudflare/cloudflare-go/page_rules.go
new file mode 100644
index 000000000..36f62e62f
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/page_rules.go
@@ -0,0 +1,235 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// PageRuleTarget is the target to evaluate on a request.
+//
+// Currently Target must always be "url" and Operator must be "matches". Value
+// is the URL pattern to match against.
+type PageRuleTarget struct {
+	Target     string `json:"target"`
+	Constraint struct {
+		Operator string `json:"operator"`
+		Value    string `json:"value"`
+	} `json:"constraint"`
+}
+
+/*
+PageRuleAction is the action to take when the target is matched.
+
+Valid IDs are:
+  always_online
+  always_use_https
+  automatic_https_rewrites
+  browser_cache_ttl
+  browser_check
+  bypass_cache_on_cookie
+  cache_by_device_type
+  cache_deception_armor
+  cache_level
+  cache_on_cookie
+  disable_apps
+  disable_performance
+  disable_railgun
+  disable_security
+  edge_cache_ttl
+  email_obfuscation
+  explicit_cache_control
+  forwarding_url
+  host_header_override
+  ip_geolocation
+  minify
+  mirage
+  opportunistic_encryption
+  origin_error_page_pass_thru
+  polish
+  resolve_override
+  respect_strong_etag
+  response_buffering
+  rocket_loader
+  security_level
+  server_side_exclude
+  sort_query_string_for_cache
+  ssl
+  true_client_ip_header
+  waf
+*/
+type PageRuleAction struct {
+	ID    string      `json:"id"`
+	Value interface{} `json:"value"`
+}
+
+// PageRuleActions maps API action IDs to human-readable strings.
+var PageRuleActions = map[string]string{
+	"always_online":               "Always Online",               // Value of type string
+	"always_use_https":            "Always Use HTTPS",            // Value of type interface{}
+	"automatic_https_rewrites":    "Automatic HTTPS Rewrites",    // Value of type string
+	"browser_cache_ttl":           "Browser Cache TTL",           // Value of type int
+	"browser_check":               "Browser Integrity Check",     // Value of type string
+	"bypass_cache_on_cookie":      "Bypass Cache on Cookie",      // Value of type string
+	"cache_by_device_type":        "Cache By Device Type",        // Value of type string
+	"cache_deception_armor":       "Cache Deception Armor",       // Value of type string
+	"cache_level":                 "Cache Level",                 // Value of type string
+	"cache_on_cookie":             "Cache On Cookie",             // Value of type string
+	"disable_apps":                "Disable Apps",                // Value of type interface{}
+	"disable_performance":         "Disable Performance",         // Value of type interface{}
+	"disable_railgun":             "Disable Railgun",             // Value of type string
+	"disable_security":            "Disable Security",            // Value of type interface{}
+	"edge_cache_ttl":              "Edge Cache TTL",              // Value of type int
+	"email_obfuscation":           "Email Obfuscation",           // Value of type string
+	"explicit_cache_control":      "Origin Cache Control",        // Value of type string
+	"forwarding_url":              "Forwarding URL",              // Value of type map[string]interface
+	"host_header_override":        "Host Header Override",        // Value of type string
+	"ip_geolocation":              "IP Geolocation Header",       // Value of type string
+	"minify":                      "Minify",                      // Value of type map[string]interface
+	"mirage":                      "Mirage",                      // Value of type string
+	"opportunistic_encryption":    "Opportunistic Encryption",    // Value of type string
+	"origin_error_page_pass_thru": "Origin Error Page Pass-thru", // Value of type string
+	"polish":                      "Polish",                      // Value of type string
+	"resolve_override":            "Resolve Override",            // Value of type string
+	"respect_strong_etag":         "Respect Strong ETags",        // Value of type string
+	"response_buffering":          "Response Buffering",          // Value of type string
+	"rocket_loader":               "Rocker Loader",               // Value of type string
+	"security_level":              "Security Level",              // Value of type string
+	"server_side_exclude":         "Server Side Excludes",        // Value of type string
+	"sort_query_string_for_cache": "Query String Sort",           // Value of type string
+	"ssl":                         "SSL",                         // Value of type string
+	"true_client_ip_header":       "True Client IP Header",       // Value of type string
+	"waf":                         "Web Application Firewall",    // Value of type string
+}
+
+// PageRule describes a Page Rule.
+type PageRule struct {
+	ID         string           `json:"id,omitempty"`
+	Targets    []PageRuleTarget `json:"targets"`
+	Actions    []PageRuleAction `json:"actions"`
+	Priority   int              `json:"priority"`
+	Status     string           `json:"status"` // can be: active, paused
+	ModifiedOn time.Time        `json:"modified_on,omitempty"`
+	CreatedOn  time.Time        `json:"created_on,omitempty"`
+}
+
+// PageRuleDetailResponse is the API response, containing a single PageRule.
+type PageRuleDetailResponse struct {
+	Success  bool     `json:"success"`
+	Errors   []string `json:"errors"`
+	Messages []string `json:"messages"`
+	Result   PageRule `json:"result"`
+}
+
+// PageRulesResponse is the API response, containing an array of PageRules.
+type PageRulesResponse struct {
+	Success  bool       `json:"success"`
+	Errors   []string   `json:"errors"`
+	Messages []string   `json:"messages"`
+	Result   []PageRule `json:"result"`
+}
+
+// CreatePageRule creates a new Page Rule for a zone.
+//
+// API reference: https://api.cloudflare.com/#page-rules-for-a-zone-create-a-page-rule
+func (api *API) CreatePageRule(zoneID string, rule PageRule) (*PageRule, error) {
+	uri := "/zones/" + zoneID + "/pagerules"
+	res, err := api.makeRequest("POST", uri, rule)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r PageRuleDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return &r.Result, nil
+}
+
+// ListPageRules returns all Page Rules for a zone.
+//
+// API reference: https://api.cloudflare.com/#page-rules-for-a-zone-list-page-rules
+func (api *API) ListPageRules(zoneID string) ([]PageRule, error) {
+	uri := "/zones/" + zoneID + "/pagerules"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []PageRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r PageRulesResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []PageRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// PageRule fetches detail about one Page Rule for a zone.
+//
+// API reference: https://api.cloudflare.com/#page-rules-for-a-zone-page-rule-details
+func (api *API) PageRule(zoneID, ruleID string) (PageRule, error) {
+	uri := "/zones/" + zoneID + "/pagerules/" + ruleID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return PageRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r PageRuleDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return PageRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ChangePageRule lets you change individual settings for a Page Rule. This is
+// in contrast to UpdatePageRule which replaces the entire Page Rule.
+//
+// API reference: https://api.cloudflare.com/#page-rules-for-a-zone-change-a-page-rule
+func (api *API) ChangePageRule(zoneID, ruleID string, rule PageRule) error {
+	uri := "/zones/" + zoneID + "/pagerules/" + ruleID
+	res, err := api.makeRequest("PATCH", uri, rule)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r PageRuleDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
+
+// UpdatePageRule lets you replace a Page Rule. This is in contrast to
+// ChangePageRule which lets you change individual settings.
+//
+// API reference: https://api.cloudflare.com/#page-rules-for-a-zone-update-a-page-rule
+func (api *API) UpdatePageRule(zoneID, ruleID string, rule PageRule) error {
+	uri := "/zones/" + zoneID + "/pagerules/" + ruleID
+	res, err := api.makeRequest("PUT", uri, rule)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r PageRuleDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
+
+// DeletePageRule deletes a Page Rule for a zone.
+//
+// API reference: https://api.cloudflare.com/#page-rules-for-a-zone-delete-a-page-rule
+func (api *API) DeletePageRule(zoneID, ruleID string) error {
+	uri := "/zones/" + zoneID + "/pagerules/" + ruleID
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r PageRuleDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/railgun.go b/vendor/github.com/cloudflare/cloudflare-go/railgun.go
new file mode 100644
index 000000000..72d228691
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/railgun.go
@@ -0,0 +1,297 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// Railgun represents a Railgun's properties.
+type Railgun struct {
+	ID             string    `json:"id"`
+	Name           string    `json:"name"`
+	Status         string    `json:"status"`
+	Enabled        bool      `json:"enabled"`
+	ZonesConnected int       `json:"zones_connected"`
+	Build          string    `json:"build"`
+	Version        string    `json:"version"`
+	Revision       string    `json:"revision"`
+	ActivationKey  string    `json:"activation_key"`
+	ActivatedOn    time.Time `json:"activated_on"`
+	CreatedOn      time.Time `json:"created_on"`
+	ModifiedOn     time.Time `json:"modified_on"`
+	UpgradeInfo    struct {
+		LatestVersion string `json:"latest_version"`
+		DownloadLink  string `json:"download_link"`
+	} `json:"upgrade_info"`
+}
+
+// RailgunListOptions represents the parameters used to list railguns.
+type RailgunListOptions struct {
+	Direction string
+}
+
+// railgunResponse represents the response from the Create Railgun and the Railgun Details endpoints.
+type railgunResponse struct {
+	Response
+	Result Railgun `json:"result"`
+}
+
+// railgunsResponse represents the response from the List Railguns endpoint.
+type railgunsResponse struct {
+	Response
+	Result []Railgun `json:"result"`
+}
+
+// CreateRailgun creates a new Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-create-railgun
+func (api *API) CreateRailgun(name string) (Railgun, error) {
+	uri := api.userBaseURL("") + "/railguns"
+	params := struct {
+		Name string `json:"name"`
+	}{
+		Name: name,
+	}
+	res, err := api.makeRequest("POST", uri, params)
+	if err != nil {
+		return Railgun{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r railgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return Railgun{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListRailguns lists Railguns connected to an account.
+//
+// API reference: https://api.cloudflare.com/#railgun-list-railguns
+func (api *API) ListRailguns(options RailgunListOptions) ([]Railgun, error) {
+	v := url.Values{}
+	if options.Direction != "" {
+		v.Set("direction", options.Direction)
+	}
+	uri := api.userBaseURL("") + "/railguns" + "?" + v.Encode()
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r railgunsResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// RailgunDetails returns the details for a Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-railgun-details
+func (api *API) RailgunDetails(railgunID string) (Railgun, error) {
+	uri := api.userBaseURL("") + "/railguns/" + railgunID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return Railgun{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r railgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return Railgun{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// RailgunZones returns the zones that are currently using a Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-get-zones-connected-to-a-railgun
+func (api *API) RailgunZones(railgunID string) ([]Zone, error) {
+	uri := api.userBaseURL("") + "/railguns/" + railgunID + "/zones"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r ZonesResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// enableRailgun enables (true) or disables (false) a Railgun for all zones connected to it.
+//
+// API reference: https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun
+func (api *API) enableRailgun(railgunID string, enable bool) (Railgun, error) {
+	uri := api.userBaseURL("") + "/railguns/" + railgunID
+	params := struct {
+		Enabled bool `json:"enabled"`
+	}{
+		Enabled: enable,
+	}
+	res, err := api.makeRequest("PATCH", uri, params)
+	if err != nil {
+		return Railgun{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r railgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return Railgun{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// EnableRailgun enables a Railgun for all zones connected to it.
+//
+// API reference: https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun
+func (api *API) EnableRailgun(railgunID string) (Railgun, error) {
+	return api.enableRailgun(railgunID, true)
+}
+
+// DisableRailgun enables a Railgun for all zones connected to it.
+//
+// API reference: https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun
+func (api *API) DisableRailgun(railgunID string) (Railgun, error) {
+	return api.enableRailgun(railgunID, false)
+}
+
+// DeleteRailgun disables and deletes a Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-delete-railgun
+func (api *API) DeleteRailgun(railgunID string) error {
+	uri := api.userBaseURL("") + "/railguns/" + railgunID
+	if _, err := api.makeRequest("DELETE", uri, nil); err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	return nil
+}
+
+// ZoneRailgun represents the status of a Railgun on a zone.
+type ZoneRailgun struct {
+	ID        string `json:"id"`
+	Name      string `json:"name"`
+	Enabled   bool   `json:"enabled"`
+	Connected bool   `json:"connected"`
+}
+
+// zoneRailgunResponse represents the response from the Zone Railgun Details endpoint.
+type zoneRailgunResponse struct {
+	Response
+	Result ZoneRailgun `json:"result"`
+}
+
+// zoneRailgunsResponse represents the response from the Zone Railgun endpoint.
+type zoneRailgunsResponse struct {
+	Response
+	Result []ZoneRailgun `json:"result"`
+}
+
+// RailgunDiagnosis represents the test results from testing railgun connections
+// to a zone.
+type RailgunDiagnosis struct {
+	Method          string `json:"method"`
+	HostName        string `json:"host_name"`
+	HTTPStatus      int    `json:"http_status"`
+	Railgun         string `json:"railgun"`
+	URL             string `json:"url"`
+	ResponseStatus  string `json:"response_status"`
+	Protocol        string `json:"protocol"`
+	ElapsedTime     string `json:"elapsed_time"`
+	BodySize        string `json:"body_size"`
+	BodyHash        string `json:"body_hash"`
+	MissingHeaders  string `json:"missing_headers"`
+	ConnectionClose bool   `json:"connection_close"`
+	Cloudflare      string `json:"cloudflare"`
+	CFRay           string `json:"cf-ray"`
+	// NOTE: Cloudflare's online API documentation does not yet have definitions
+	// for the following fields. See: https://api.cloudflare.com/#railgun-connections-for-a-zone-test-railgun-connection/
+	CFWANError    string `json:"cf-wan-error"`
+	CFCacheStatus string `json:"cf-cache-status"`
+}
+
+// railgunDiagnosisResponse represents the response from the Test Railgun Connection enpoint.
+type railgunDiagnosisResponse struct {
+	Response
+	Result RailgunDiagnosis `json:"result"`
+}
+
+// ZoneRailguns returns the available Railguns for a zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-get-available-railguns
+func (api *API) ZoneRailguns(zoneID string) ([]ZoneRailgun, error) {
+	uri := "/zones/" + zoneID + "/railguns"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneRailgunsResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ZoneRailgunDetails returns the configuration for a given Railgun.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-get-railgun-details
+func (api *API) ZoneRailgunDetails(zoneID, railgunID string) (ZoneRailgun, error) {
+	uri := "/zones/" + zoneID + "/railguns/" + railgunID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return ZoneRailgun{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneRailgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return ZoneRailgun{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// TestRailgunConnection tests a Railgun connection for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railgun-connections-for-a-zone-test-railgun-connection
+func (api *API) TestRailgunConnection(zoneID, railgunID string) (RailgunDiagnosis, error) {
+	uri := "/zones/" + zoneID + "/railguns/" + railgunID + "/diagnose"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return RailgunDiagnosis{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r railgunDiagnosisResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return RailgunDiagnosis{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// connectZoneRailgun connects (true) or disconnects (false) a Railgun for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun
+func (api *API) connectZoneRailgun(zoneID, railgunID string, connect bool) (ZoneRailgun, error) {
+	uri := "/zones/" + zoneID + "/railguns/" + railgunID
+	params := struct {
+		Connected bool `json:"connected"`
+	}{
+		Connected: connect,
+	}
+	res, err := api.makeRequest("PATCH", uri, params)
+	if err != nil {
+		return ZoneRailgun{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneRailgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return ZoneRailgun{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ConnectZoneRailgun connects a Railgun for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun
+func (api *API) ConnectZoneRailgun(zoneID, railgunID string) (ZoneRailgun, error) {
+	return api.connectZoneRailgun(zoneID, railgunID, true)
+}
+
+// DisconnectZoneRailgun disconnects a Railgun for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun
+func (api *API) DisconnectZoneRailgun(zoneID, railgunID string) (ZoneRailgun, error) {
+	return api.connectZoneRailgun(zoneID, railgunID, false)
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/rate_limiting.go b/vendor/github.com/cloudflare/cloudflare-go/rate_limiting.go
new file mode 100644
index 000000000..e3eb3e2e7
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/rate_limiting.go
@@ -0,0 +1,210 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+// RateLimit is a policy than can be applied to limit traffic within a customer domain
+type RateLimit struct {
+	ID          string                  `json:"id,omitempty"`
+	Disabled    bool                    `json:"disabled,omitempty"`
+	Description string                  `json:"description,omitempty"`
+	Match       RateLimitTrafficMatcher `json:"match"`
+	Bypass      []RateLimitKeyValue     `json:"bypass,omitempty"`
+	Threshold   int                     `json:"threshold"`
+	Period      int                     `json:"period"`
+	Action      RateLimitAction         `json:"action"`
+	Correlate   *RateLimitCorrelate     `json:"correlate,omitempty"`
+}
+
+// RateLimitTrafficMatcher contains the rules that will be used to apply a rate limit to traffic
+type RateLimitTrafficMatcher struct {
+	Request  RateLimitRequestMatcher  `json:"request"`
+	Response RateLimitResponseMatcher `json:"response"`
+}
+
+// RateLimitRequestMatcher contains the matching rules pertaining to requests
+type RateLimitRequestMatcher struct {
+	Methods    []string `json:"methods,omitempty"`
+	Schemes    []string `json:"schemes,omitempty"`
+	URLPattern string   `json:"url,omitempty"`
+}
+
+// RateLimitResponseMatcher contains the matching rules pertaining to responses
+type RateLimitResponseMatcher struct {
+	Statuses      []int                            `json:"status,omitempty"`
+	OriginTraffic *bool                            `json:"origin_traffic,omitempty"` // api defaults to true so we need an explicit empty value
+	Headers       []RateLimitResponseMatcherHeader `json:"headers,omitempty"`
+}
+
+// RateLimitResponseMatcherHeader contains the structure of the origin
+// HTTP headers used in request matcher checks.
+type RateLimitResponseMatcherHeader struct {
+	Name  string `json:"name"`
+	Op    string `json:"op"`
+	Value string `json:"value"`
+}
+
+// RateLimitKeyValue is k-v formatted as expected in the rate limit description
+type RateLimitKeyValue struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
+// RateLimitAction is the action that will be taken when the rate limit threshold is reached
+type RateLimitAction struct {
+	Mode     string                   `json:"mode"`
+	Timeout  int                      `json:"timeout"`
+	Response *RateLimitActionResponse `json:"response"`
+}
+
+// RateLimitActionResponse is the response that will be returned when rate limit action is triggered
+type RateLimitActionResponse struct {
+	ContentType string `json:"content_type"`
+	Body        string `json:"body"`
+}
+
+// RateLimitCorrelate pertainings to NAT support
+type RateLimitCorrelate struct {
+	By string `json:"by"`
+}
+
+type rateLimitResponse struct {
+	Response
+	Result RateLimit `json:"result"`
+}
+
+type rateLimitListResponse struct {
+	Response
+	Result     []RateLimit `json:"result"`
+	ResultInfo ResultInfo  `json:"result_info"`
+}
+
+// CreateRateLimit creates a new rate limit for a zone.
+//
+// API reference: https://api.cloudflare.com/#rate-limits-for-a-zone-create-a-ratelimit
+func (api *API) CreateRateLimit(zoneID string, limit RateLimit) (RateLimit, error) {
+	uri := "/zones/" + zoneID + "/rate_limits"
+	res, err := api.makeRequest("POST", uri, limit)
+	if err != nil {
+		return RateLimit{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r rateLimitResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return RateLimit{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListRateLimits returns Rate Limits for a zone, paginated according to the provided options
+//
+// API reference: https://api.cloudflare.com/#rate-limits-for-a-zone-list-rate-limits
+func (api *API) ListRateLimits(zoneID string, pageOpts PaginationOptions) ([]RateLimit, ResultInfo, error) {
+	v := url.Values{}
+	if pageOpts.PerPage > 0 {
+		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
+	}
+	if pageOpts.Page > 0 {
+		v.Set("page", strconv.Itoa(pageOpts.Page))
+	}
+
+	uri := "/zones/" + zoneID + "/rate_limits"
+	if len(v) > 0 {
+		uri = uri + "?" + v.Encode()
+	}
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []RateLimit{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r rateLimitListResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []RateLimit{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, r.ResultInfo, nil
+}
+
+// ListAllRateLimits returns all Rate Limits for a zone.
+//
+// API reference: https://api.cloudflare.com/#rate-limits-for-a-zone-list-rate-limits
+func (api *API) ListAllRateLimits(zoneID string) ([]RateLimit, error) {
+	pageOpts := PaginationOptions{
+		PerPage: 100, // this is the max page size allowed
+		Page:    1,
+	}
+
+	allRateLimits := make([]RateLimit, 0)
+	for {
+		rateLimits, resultInfo, err := api.ListRateLimits(zoneID, pageOpts)
+		if err != nil {
+			return []RateLimit{}, err
+		}
+		allRateLimits = append(allRateLimits, rateLimits...)
+		// total pages is not returned on this call
+		// if number of records is less than the max, this must be the last page
+		// in case TotalCount % PerPage = 0, the last request will return an empty list
+		if resultInfo.Count < resultInfo.PerPage {
+			break
+		}
+		// continue with the next page
+		pageOpts.Page = pageOpts.Page + 1
+	}
+
+	return allRateLimits, nil
+}
+
+// RateLimit fetches detail about one Rate Limit for a zone.
+//
+// API reference: https://api.cloudflare.com/#rate-limits-for-a-zone-rate-limit-details
+func (api *API) RateLimit(zoneID, limitID string) (RateLimit, error) {
+	uri := "/zones/" + zoneID + "/rate_limits/" + limitID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return RateLimit{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r rateLimitResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return RateLimit{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// UpdateRateLimit lets you replace a Rate Limit for a zone.
+//
+// API reference: https://api.cloudflare.com/#rate-limits-for-a-zone-update-rate-limit
+func (api *API) UpdateRateLimit(zoneID, limitID string, limit RateLimit) (RateLimit, error) {
+	uri := "/zones/" + zoneID + "/rate_limits/" + limitID
+	res, err := api.makeRequest("PUT", uri, limit)
+	if err != nil {
+		return RateLimit{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r rateLimitResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return RateLimit{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// DeleteRateLimit deletes a Rate Limit for a zone.
+//
+// API reference: https://api.cloudflare.com/#rate-limits-for-a-zone-delete-rate-limit
+func (api *API) DeleteRateLimit(zoneID, limitID string) error {
+	uri := "/zones/" + zoneID + "/rate_limits/" + limitID
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	var r rateLimitResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/registrar.go b/vendor/github.com/cloudflare/cloudflare-go/registrar.go
new file mode 100644
index 000000000..51eacf173
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/registrar.go
@@ -0,0 +1,175 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// RegistrarDomain is the structure of the API response for a new
+// Cloudflare Registrar domain.
+type RegistrarDomain struct {
+	ID                string              `json:"id"`
+	Available         bool                `json:"available"`
+	SupportedTLD      bool                `json:"supported_tld"`
+	CanRegister       bool                `json:"can_register"`
+	TransferIn        RegistrarTransferIn `json:"transfer_in"`
+	CurrentRegistrar  string              `json:"current_registrar"`
+	ExpiresAt         time.Time           `json:"expires_at"`
+	RegistryStatuses  string              `json:"registry_statuses"`
+	Locked            bool                `json:"locked"`
+	CreatedAt         time.Time           `json:"created_at"`
+	UpdatedAt         time.Time           `json:"updated_at"`
+	RegistrantContact RegistrantContact   `json:"registrant_contact"`
+}
+
+// RegistrarTransferIn contains the structure for a domain transfer in
+// request.
+type RegistrarTransferIn struct {
+	UnlockDomain      string `json:"unlock_domain"`
+	DisablePrivacy    string `json:"disable_privacy"`
+	EnterAuthCode     string `json:"enter_auth_code"`
+	ApproveTransfer   string `json:"approve_transfer"`
+	AcceptFoa         string `json:"accept_foa"`
+	CanCancelTransfer bool   `json:"can_cancel_transfer"`
+}
+
+// RegistrantContact is the contact details for the domain registration.
+type RegistrantContact struct {
+	ID           string `json:"id"`
+	FirstName    string `json:"first_name"`
+	LastName     string `json:"last_name"`
+	Organization string `json:"organization"`
+	Address      string `json:"address"`
+	Address2     string `json:"address2"`
+	City         string `json:"city"`
+	State        string `json:"state"`
+	Zip          string `json:"zip"`
+	Country      string `json:"country"`
+	Phone        string `json:"phone"`
+	Email        string `json:"email"`
+	Fax          string `json:"fax"`
+}
+
+// RegistrarDomainConfiguration is the structure for making updates to
+// and existing domain.
+type RegistrarDomainConfiguration struct {
+	NameServers []string `json:"name_servers"`
+	Privacy     bool     `json:"privacy"`
+	Locked      bool     `json:"locked"`
+	AutoRenew   bool     `json:"auto_renew"`
+}
+
+// RegistrarDomainDetailResponse is the structure of the detailed
+// response from the API for a single domain.
+type RegistrarDomainDetailResponse struct {
+	Response
+	Result RegistrarDomain `json:"result"`
+}
+
+// RegistrarDomainsDetailResponse is the structure of the detailed
+// response from the API.
+type RegistrarDomainsDetailResponse struct {
+	Response
+	Result []RegistrarDomain `json:"result"`
+}
+
+// RegistrarDomain returns a single domain based on the account ID and
+// domain name.
+//
+// API reference: https://api.cloudflare.com/#registrar-domains-get-domain
+func (api *API) RegistrarDomain(accountID, domainName string) (RegistrarDomain, error) {
+	uri := fmt.Sprintf("/accounts/%s/registrar/domains/%s", accountID, domainName)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return RegistrarDomain{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r RegistrarDomainDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return RegistrarDomain{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// RegistrarDomains returns all registrar domains based on the account
+// ID.
+//
+// API reference: https://api.cloudflare.com/#registrar-domains-list-domains
+func (api *API) RegistrarDomains(accountID string) ([]RegistrarDomain, error) {
+	uri := "/accounts/" + accountID + "/registrar/domains"
+
+	res, err := api.makeRequest("POST", uri, nil)
+	if err != nil {
+		return []RegistrarDomain{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r RegistrarDomainsDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []RegistrarDomain{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// TransferRegistrarDomain initiates the transfer from another registrar
+// to Cloudflare Registrar.
+//
+// API reference: https://api.cloudflare.com/#registrar-domains-transfer-domain
+func (api *API) TransferRegistrarDomain(accountID, domainName string) ([]RegistrarDomain, error) {
+	uri := fmt.Sprintf("/accounts/%s/registrar/domains/%s/transfer", accountID, domainName)
+
+	res, err := api.makeRequest("POST", uri, nil)
+	if err != nil {
+		return []RegistrarDomain{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r RegistrarDomainsDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []RegistrarDomain{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// CancelRegistrarDomainTransfer cancels a pending domain transfer.
+//
+// API reference: https://api.cloudflare.com/#registrar-domains-cancel-transfer
+func (api *API) CancelRegistrarDomainTransfer(accountID, domainName string) ([]RegistrarDomain, error) {
+	uri := fmt.Sprintf("/accounts/%s/registrar/domains/%s/cancel_transfer", accountID, domainName)
+
+	res, err := api.makeRequest("POST", uri, nil)
+	if err != nil {
+		return []RegistrarDomain{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r RegistrarDomainsDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []RegistrarDomain{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// UpdateRegistrarDomain updates an existing Registrar Domain configuration.
+//
+// API reference: https://api.cloudflare.com/#registrar-domains-update-domain
+func (api *API) UpdateRegistrarDomain(accountID, domainName string, domainConfiguration RegistrarDomainConfiguration) (RegistrarDomain, error) {
+	uri := fmt.Sprintf("/accounts/%s/registrar/domains/%s", accountID, domainName)
+
+	res, err := api.makeRequest("PUT", uri, domainConfiguration)
+	if err != nil {
+		return RegistrarDomain{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r RegistrarDomainDetailResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return RegistrarDomain{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/renovate.json b/vendor/github.com/cloudflare/cloudflare-go/renovate.json
new file mode 100644
index 000000000..f45d8f110
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/renovate.json
@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "config:base"
+  ]
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/spectrum.go b/vendor/github.com/cloudflare/cloudflare-go/spectrum.go
new file mode 100644
index 000000000..a95a2cd7f
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/spectrum.go
@@ -0,0 +1,158 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// SpectrumApplication defines a single Spectrum Application.
+type SpectrumApplication struct {
+	ID            string                        `json:"id,omitempty"`
+	Protocol      string                        `json:"protocol,omitempty"`
+	IPv4          bool                          `json:"ipv4,omitempty"`
+	DNS           SpectrumApplicationDNS        `json:"dns,omitempty"`
+	OriginDirect  []string                      `json:"origin_direct,omitempty"`
+	OriginPort    int                           `json:"origin_port,omitempty"`
+	OriginDNS     *SpectrumApplicationOriginDNS `json:"origin_dns,omitempty"`
+	IPFirewall    bool                          `json:"ip_firewall,omitempty"`
+	ProxyProtocol bool                          `json:"proxy_protocol,omitempty"`
+	TLS           string                        `json:"tls,omitempty"`
+	CreatedOn     *time.Time                    `json:"created_on,omitempty"`
+	ModifiedOn    *time.Time                    `json:"modified_on,omitempty"`
+}
+
+// SpectrumApplicationDNS holds the external DNS configuration for a Spectrum
+// Application.
+type SpectrumApplicationDNS struct {
+	Type string `json:"type"`
+	Name string `json:"name"`
+}
+
+// SpectrumApplicationOriginDNS holds the origin DNS configuration for a Spectrum
+// Application.
+type SpectrumApplicationOriginDNS struct {
+	Name string `json:"name"`
+}
+
+// SpectrumApplicationDetailResponse is the structure of the detailed response
+// from the API.
+type SpectrumApplicationDetailResponse struct {
+	Response
+	Result SpectrumApplication `json:"result"`
+}
+
+// SpectrumApplicationsDetailResponse is the structure of the detailed response
+// from the API.
+type SpectrumApplicationsDetailResponse struct {
+	Response
+	Result []SpectrumApplication `json:"result"`
+}
+
+// SpectrumApplications fetches all of the Spectrum applications for a zone.
+//
+// API reference: https://developers.cloudflare.com/spectrum/api-reference/#list-spectrum-applications
+func (api *API) SpectrumApplications(zoneID string) ([]SpectrumApplication, error) {
+	uri := "/zones/" + zoneID + "/spectrum/apps"
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []SpectrumApplication{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var spectrumApplications SpectrumApplicationsDetailResponse
+	err = json.Unmarshal(res, &spectrumApplications)
+	if err != nil {
+		return []SpectrumApplication{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return spectrumApplications.Result, nil
+}
+
+// SpectrumApplication fetches a single Spectrum application based on the ID.
+//
+// API reference: https://developers.cloudflare.com/spectrum/api-reference/#list-spectrum-applications
+func (api *API) SpectrumApplication(zoneID string, applicationID string) (SpectrumApplication, error) {
+	uri := fmt.Sprintf(
+		"/zones/%s/spectrum/apps/%s",
+		zoneID,
+		applicationID,
+	)
+
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return SpectrumApplication{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var spectrumApplication SpectrumApplicationDetailResponse
+	err = json.Unmarshal(res, &spectrumApplication)
+	if err != nil {
+		return SpectrumApplication{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return spectrumApplication.Result, nil
+}
+
+// CreateSpectrumApplication creates a new Spectrum application.
+//
+// API reference: https://developers.cloudflare.com/spectrum/api-reference/#create-a-spectrum-application
+func (api *API) CreateSpectrumApplication(zoneID string, appDetails SpectrumApplication) (SpectrumApplication, error) {
+	uri := "/zones/" + zoneID + "/spectrum/apps"
+
+	res, err := api.makeRequest("POST", uri, appDetails)
+	if err != nil {
+		return SpectrumApplication{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var spectrumApplication SpectrumApplicationDetailResponse
+	err = json.Unmarshal(res, &spectrumApplication)
+	if err != nil {
+		return SpectrumApplication{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return spectrumApplication.Result, nil
+}
+
+// UpdateSpectrumApplication updates an existing Spectrum application.
+//
+// API reference: https://developers.cloudflare.com/spectrum/api-reference/#update-a-spectrum-application
+func (api *API) UpdateSpectrumApplication(zoneID, appID string, appDetails SpectrumApplication) (SpectrumApplication, error) {
+	uri := fmt.Sprintf(
+		"/zones/%s/spectrum/apps/%s",
+		zoneID,
+		appID,
+	)
+
+	res, err := api.makeRequest("PUT", uri, appDetails)
+	if err != nil {
+		return SpectrumApplication{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var spectrumApplication SpectrumApplicationDetailResponse
+	err = json.Unmarshal(res, &spectrumApplication)
+	if err != nil {
+		return SpectrumApplication{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return spectrumApplication.Result, nil
+}
+
+// DeleteSpectrumApplication removes a Spectrum application based on the ID.
+//
+// API reference: https://developers.cloudflare.com/spectrum/api-reference/#delete-a-spectrum-application
+func (api *API) DeleteSpectrumApplication(zoneID string, applicationID string) error {
+	uri := fmt.Sprintf(
+		"/zones/%s/spectrum/apps/%s",
+		zoneID,
+		applicationID,
+	)
+
+	_, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/ssl.go b/vendor/github.com/cloudflare/cloudflare-go/ssl.go
new file mode 100644
index 000000000..505dfa650
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/ssl.go
@@ -0,0 +1,157 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// ZoneCustomSSL represents custom SSL certificate metadata.
+type ZoneCustomSSL struct {
+	ID              string                       `json:"id"`
+	Hosts           []string                     `json:"hosts"`
+	Issuer          string                       `json:"issuer"`
+	Signature       string                       `json:"signature"`
+	Status          string                       `json:"status"`
+	BundleMethod    string                       `json:"bundle_method"`
+	GeoRestrictions ZoneCustomSSLGeoRestrictions `json:"geo_restrictions"`
+	ZoneID          string                       `json:"zone_id"`
+	UploadedOn      time.Time                    `json:"uploaded_on"`
+	ModifiedOn      time.Time                    `json:"modified_on"`
+	ExpiresOn       time.Time                    `json:"expires_on"`
+	Priority        int                          `json:"priority"`
+	KeylessServer   KeylessSSL                   `json:"keyless_server"`
+}
+
+// ZoneCustomSSLGeoRestrictions represents the parameter to create or update
+// geographic restrictions on a custom ssl certificate.
+type ZoneCustomSSLGeoRestrictions struct {
+	Label string `json:"label"`
+}
+
+// zoneCustomSSLResponse represents the response from the zone SSL details endpoint.
+type zoneCustomSSLResponse struct {
+	Response
+	Result ZoneCustomSSL `json:"result"`
+}
+
+// zoneCustomSSLsResponse represents the response from the zone SSL list endpoint.
+type zoneCustomSSLsResponse struct {
+	Response
+	Result []ZoneCustomSSL `json:"result"`
+}
+
+// ZoneCustomSSLOptions represents the parameters to create or update an existing
+// custom SSL configuration.
+type ZoneCustomSSLOptions struct {
+	Certificate     string                       `json:"certificate"`
+	PrivateKey      string                       `json:"private_key"`
+	BundleMethod    string                       `json:"bundle_method,omitempty"`
+	GeoRestrictions ZoneCustomSSLGeoRestrictions `json:"geo_restrictions,omitempty"`
+	Type            string                       `json:"type,omitempty"`
+}
+
+// ZoneCustomSSLPriority represents a certificate's ID and priority. It is a
+// subset of ZoneCustomSSL used for patch requests.
+type ZoneCustomSSLPriority struct {
+	ID       string `json:"ID"`
+	Priority int    `json:"priority"`
+}
+
+// CreateSSL allows you to add a custom SSL certificate to the given zone.
+//
+// API reference: https://api.cloudflare.com/#custom-ssl-for-a-zone-create-ssl-configuration
+func (api *API) CreateSSL(zoneID string, options ZoneCustomSSLOptions) (ZoneCustomSSL, error) {
+	uri := "/zones/" + zoneID + "/custom_certificates"
+	res, err := api.makeRequest("POST", uri, options)
+	if err != nil {
+		return ZoneCustomSSL{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneCustomSSLResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return ZoneCustomSSL{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListSSL lists the custom certificates for the given zone.
+//
+// API reference: https://api.cloudflare.com/#custom-ssl-for-a-zone-list-ssl-configurations
+func (api *API) ListSSL(zoneID string) ([]ZoneCustomSSL, error) {
+	uri := "/zones/" + zoneID + "/custom_certificates"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneCustomSSLsResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// SSLDetails returns the configuration details for a custom SSL certificate.
+//
+// API reference: https://api.cloudflare.com/#custom-ssl-for-a-zone-ssl-configuration-details
+func (api *API) SSLDetails(zoneID, certificateID string) (ZoneCustomSSL, error) {
+	uri := "/zones/" + zoneID + "/custom_certificates/" + certificateID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return ZoneCustomSSL{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneCustomSSLResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return ZoneCustomSSL{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// UpdateSSL updates (replaces) a custom SSL certificate.
+//
+// API reference: https://api.cloudflare.com/#custom-ssl-for-a-zone-update-ssl-configuration
+func (api *API) UpdateSSL(zoneID, certificateID string, options ZoneCustomSSLOptions) (ZoneCustomSSL, error) {
+	uri := "/zones/" + zoneID + "/custom_certificates/" + certificateID
+	res, err := api.makeRequest("PATCH", uri, options)
+	if err != nil {
+		return ZoneCustomSSL{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneCustomSSLResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return ZoneCustomSSL{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ReprioritizeSSL allows you to change the priority (which is served for a given
+// request) of custom SSL certificates associated with the given zone.
+//
+// API reference: https://api.cloudflare.com/#custom-ssl-for-a-zone-re-prioritize-ssl-certificates
+func (api *API) ReprioritizeSSL(zoneID string, p []ZoneCustomSSLPriority) ([]ZoneCustomSSL, error) {
+	uri := "/zones/" + zoneID + "/custom_certificates/prioritize"
+	params := struct {
+		Certificates []ZoneCustomSSLPriority `json:"certificates"`
+	}{
+		Certificates: p,
+	}
+	res, err := api.makeRequest("PUT", uri, params)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneCustomSSLsResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// DeleteSSL deletes a custom SSL certificate from the given zone.
+//
+// API reference: https://api.cloudflare.com/#custom-ssl-for-a-zone-delete-an-ssl-certificate
+func (api *API) DeleteSSL(zoneID, certificateID string) error {
+	uri := "/zones/" + zoneID + "/custom_certificates/" + certificateID
+	if _, err := api.makeRequest("DELETE", uri, nil); err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+	return nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/universal_ssl.go b/vendor/github.com/cloudflare/cloudflare-go/universal_ssl.go
new file mode 100644
index 000000000..4bf8ffde7
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/universal_ssl.go
@@ -0,0 +1,88 @@
+package cloudflare
+
+import (
+	"encoding/json"
+
+	"github.com/pkg/errors"
+)
+
+// UniversalSSLSetting represents a universal ssl setting's properties.
+type UniversalSSLSetting struct {
+	Enabled bool `json:"enabled"`
+}
+
+type universalSSLSettingResponse struct {
+	Response
+	Result UniversalSSLSetting `json:"result"`
+}
+
+// UniversalSSLVerificationDetails represents a universal ssl verifcation's properties.
+type UniversalSSLVerificationDetails struct {
+	CertificateStatus  string                       `json:"certificate_status"`
+	VerificationType   string                       `json:"verification_type"`
+	ValidationMethod   string                       `json:"validation_method"`
+	CertPackUUID       string                       `json:"cert_pack_uuid"`
+	VerificationStatus bool                         `json:"verification_status"`
+	BrandCheck         bool                         `json:"brand_check"`
+	VerificationInfo   UniversalSSLVerificationInfo `json:"verification_info"`
+}
+
+// UniversalSSLVerificationInfo represents DCV record.
+type UniversalSSLVerificationInfo struct {
+	RecordName   string `json:"record_name"`
+	RecordTarget string `json:"record_target"`
+}
+
+type universalSSLVerificationResponse struct {
+	Response
+	Result []UniversalSSLVerificationDetails `json:"result"`
+}
+
+// UniversalSSLSettingDetails returns the details for a universal ssl setting
+//
+// API reference: https://api.cloudflare.com/#universal-ssl-settings-for-a-zone-universal-ssl-settings-details
+func (api *API) UniversalSSLSettingDetails(zoneID string) (UniversalSSLSetting, error) {
+	uri := "/zones/" + zoneID + "/ssl/universal/settings"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return UniversalSSLSetting{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r universalSSLSettingResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return UniversalSSLSetting{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// EditUniversalSSLSetting edits the uniersal ssl setting for a zone
+//
+// API reference: https://api.cloudflare.com/#universal-ssl-settings-for-a-zone-edit-universal-ssl-settings
+func (api *API) EditUniversalSSLSetting(zoneID string, setting UniversalSSLSetting) (UniversalSSLSetting, error) {
+	uri := "/zones/" + zoneID + "/ssl/universal/settings"
+	res, err := api.makeRequest("PATCH", uri, setting)
+	if err != nil {
+		return UniversalSSLSetting{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r universalSSLSettingResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return UniversalSSLSetting{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+
+}
+
+// UniversalSSLVerificationDetails returns the details for a universal ssl verifcation
+//
+// API reference: https://api.cloudflare.com/#ssl-verification-ssl-verification-details
+func (api *API) UniversalSSLVerificationDetails(zoneID string) ([]UniversalSSLVerificationDetails, error) {
+	uri := "/zones/" + zoneID + "/ssl/verification"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []UniversalSSLVerificationDetails{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r universalSSLVerificationResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return []UniversalSSLVerificationDetails{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/user.go b/vendor/github.com/cloudflare/cloudflare-go/user.go
new file mode 100644
index 000000000..bf2f47a57
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/user.go
@@ -0,0 +1,113 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// User describes a user account.
+type User struct {
+	ID         string     `json:"id,omitempty"`
+	Email      string     `json:"email,omitempty"`
+	FirstName  string     `json:"first_name,omitempty"`
+	LastName   string     `json:"last_name,omitempty"`
+	Username   string     `json:"username,omitempty"`
+	Telephone  string     `json:"telephone,omitempty"`
+	Country    string     `json:"country,omitempty"`
+	Zipcode    string     `json:"zipcode,omitempty"`
+	CreatedOn  *time.Time `json:"created_on,omitempty"`
+	ModifiedOn *time.Time `json:"modified_on,omitempty"`
+	APIKey     string     `json:"api_key,omitempty"`
+	TwoFA      bool       `json:"two_factor_authentication_enabled,omitempty"`
+	Betas      []string   `json:"betas,omitempty"`
+	Accounts   []Account  `json:"organizations,omitempty"`
+}
+
+// UserResponse wraps a response containing User accounts.
+type UserResponse struct {
+	Response
+	Result User `json:"result"`
+}
+
+// userBillingProfileResponse wraps a response containing Billing Profile information.
+type userBillingProfileResponse struct {
+	Response
+	Result UserBillingProfile
+}
+
+// UserBillingProfile contains Billing Profile information.
+type UserBillingProfile struct {
+	ID              string     `json:"id,omitempty"`
+	FirstName       string     `json:"first_name,omitempty"`
+	LastName        string     `json:"last_name,omitempty"`
+	Address         string     `json:"address,omitempty"`
+	Address2        string     `json:"address2,omitempty"`
+	Company         string     `json:"company,omitempty"`
+	City            string     `json:"city,omitempty"`
+	State           string     `json:"state,omitempty"`
+	ZipCode         string     `json:"zipcode,omitempty"`
+	Country         string     `json:"country,omitempty"`
+	Telephone       string     `json:"telephone,omitempty"`
+	CardNumber      string     `json:"card_number,omitempty"`
+	CardExpiryYear  int        `json:"card_expiry_year,omitempty"`
+	CardExpiryMonth int        `json:"card_expiry_month,omitempty"`
+	VAT             string     `json:"vat,omitempty"`
+	CreatedOn       *time.Time `json:"created_on,omitempty"`
+	EditedOn        *time.Time `json:"edited_on,omitempty"`
+}
+
+// UserDetails provides information about the logged-in user.
+//
+// API reference: https://api.cloudflare.com/#user-user-details
+func (api *API) UserDetails() (User, error) {
+	var r UserResponse
+	res, err := api.makeRequest("GET", "/user", nil)
+	if err != nil {
+		return User{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return User{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// UpdateUser updates the properties of the given user.
+//
+// API reference: https://api.cloudflare.com/#user-update-user
+func (api *API) UpdateUser(user *User) (User, error) {
+	var r UserResponse
+	res, err := api.makeRequest("PATCH", "/user", user)
+	if err != nil {
+		return User{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return User{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// UserBillingProfile returns the billing profile of the user.
+//
+// API reference: https://api.cloudflare.com/#user-billing-profile
+func (api *API) UserBillingProfile() (UserBillingProfile, error) {
+	var r userBillingProfileResponse
+	res, err := api.makeRequest("GET", "/user/billing/profile", nil)
+	if err != nil {
+		return UserBillingProfile{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return UserBillingProfile{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/user_agent.go b/vendor/github.com/cloudflare/cloudflare-go/user_agent.go
new file mode 100644
index 000000000..6d75f3a1d
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/user_agent.go
@@ -0,0 +1,149 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+// UserAgentRule represents a User-Agent Block. These rules can be used to
+// challenge, block or whitelist specific User-Agents for a given zone.
+type UserAgentRule struct {
+	ID            string              `json:"id"`
+	Description   string              `json:"description"`
+	Mode          string              `json:"mode"`
+	Configuration UserAgentRuleConfig `json:"configuration"`
+	Paused        bool                `json:"paused"`
+}
+
+// UserAgentRuleConfig represents a Zone Lockdown config, which comprises
+// a Target ("ip" or "ip_range") and a Value (an IP address or IP+mask,
+// respectively.)
+type UserAgentRuleConfig ZoneLockdownConfig
+
+// UserAgentRuleResponse represents a response from the Zone Lockdown endpoint.
+type UserAgentRuleResponse struct {
+	Result UserAgentRule `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// UserAgentRuleListResponse represents a response from the List Zone Lockdown endpoint.
+type UserAgentRuleListResponse struct {
+	Result []UserAgentRule `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+// CreateUserAgentRule creates a User-Agent Block rule for the given zone ID.
+//
+// API reference: https://api.cloudflare.com/#user-agent-blocking-rules-create-a-useragent-rule
+func (api *API) CreateUserAgentRule(zoneID string, ld UserAgentRule) (*UserAgentRuleResponse, error) {
+	switch ld.Mode {
+	case "block", "challenge", "js_challenge", "whitelist":
+		break
+	default:
+		return nil, errors.New(`the User-Agent Block rule mode must be one of "block", "challenge", "js_challenge", "whitelist"`)
+	}
+
+	uri := "/zones/" + zoneID + "/firewall/ua_rules"
+	res, err := api.makeRequest("POST", uri, ld)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &UserAgentRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// UpdateUserAgentRule updates a User-Agent Block rule (based on the ID) for the given zone ID.
+//
+// API reference: https://api.cloudflare.com/#user-agent-blocking-rules-update-useragent-rule
+func (api *API) UpdateUserAgentRule(zoneID string, id string, ld UserAgentRule) (*UserAgentRuleResponse, error) {
+	uri := "/zones/" + zoneID + "/firewall/ua_rules/" + id
+	res, err := api.makeRequest("PUT", uri, ld)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &UserAgentRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// DeleteUserAgentRule deletes a User-Agent Block rule (based on the ID) for the given zone ID.
+//
+// API reference: https://api.cloudflare.com/#user-agent-blocking-rules-delete-useragent-rule
+func (api *API) DeleteUserAgentRule(zoneID string, id string) (*UserAgentRuleResponse, error) {
+	uri := "/zones/" + zoneID + "/firewall/ua_rules/" + id
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &UserAgentRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// UserAgentRule retrieves a User-Agent Block rule (based on the ID) for the given zone ID.
+//
+// API reference: https://api.cloudflare.com/#user-agent-blocking-rules-useragent-rule-details
+func (api *API) UserAgentRule(zoneID string, id string) (*UserAgentRuleResponse, error) {
+	uri := "/zones/" + zoneID + "/firewall/ua_rules/" + id
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &UserAgentRuleResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// ListUserAgentRules retrieves a list of User-Agent Block rules for a given zone ID by page number.
+//
+// API reference: https://api.cloudflare.com/#user-agent-blocking-rules-list-useragent-rules
+func (api *API) ListUserAgentRules(zoneID string, page int) (*UserAgentRuleListResponse, error) {
+	v := url.Values{}
+	if page <= 0 {
+		page = 1
+	}
+
+	v.Set("page", strconv.Itoa(page))
+	v.Set("per_page", strconv.Itoa(100))
+	query := "?" + v.Encode()
+
+	uri := "/zones/" + zoneID + "/firewall/ua_rules" + query
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &UserAgentRuleListResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/virtualdns.go b/vendor/github.com/cloudflare/cloudflare-go/virtualdns.go
new file mode 100644
index 000000000..f8082e1f0
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/virtualdns.go
@@ -0,0 +1,192 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// VirtualDNS represents a Virtual DNS configuration.
+type VirtualDNS struct {
+	ID                   string   `json:"id"`
+	Name                 string   `json:"name"`
+	OriginIPs            []string `json:"origin_ips"`
+	VirtualDNSIPs        []string `json:"virtual_dns_ips"`
+	MinimumCacheTTL      uint     `json:"minimum_cache_ttl"`
+	MaximumCacheTTL      uint     `json:"maximum_cache_ttl"`
+	DeprecateAnyRequests bool     `json:"deprecate_any_requests"`
+	ModifiedOn           string   `json:"modified_on"`
+}
+
+// VirtualDNSAnalyticsMetrics respresents a group of aggregated Virtual DNS metrics.
+type VirtualDNSAnalyticsMetrics struct {
+	QueryCount         *int64   `json:"queryCount"`
+	UncachedCount      *int64   `json:"uncachedCount"`
+	StaleCount         *int64   `json:"staleCount"`
+	ResponseTimeAvg    *float64 `json:"responseTimeAvg"`
+	ResponseTimeMedian *float64 `json:"responseTimeMedian"`
+	ResponseTime90th   *float64 `json:"responseTime90th"`
+	ResponseTime99th   *float64 `json:"responseTime99th"`
+}
+
+// VirtualDNSAnalytics represents a set of aggregated Virtual DNS metrics.
+// TODO: Add the queried data and not only the aggregated values.
+type VirtualDNSAnalytics struct {
+	Totals VirtualDNSAnalyticsMetrics `json:"totals"`
+	Min    VirtualDNSAnalyticsMetrics `json:"min"`
+	Max    VirtualDNSAnalyticsMetrics `json:"max"`
+}
+
+// VirtualDNSUserAnalyticsOptions represents range and dimension selection on analytics endpoint
+type VirtualDNSUserAnalyticsOptions struct {
+	Metrics []string
+	Since   *time.Time
+	Until   *time.Time
+}
+
+// VirtualDNSResponse represents a Virtual DNS response.
+type VirtualDNSResponse struct {
+	Response
+	Result *VirtualDNS `json:"result"`
+}
+
+// VirtualDNSListResponse represents an array of Virtual DNS responses.
+type VirtualDNSListResponse struct {
+	Response
+	Result []*VirtualDNS `json:"result"`
+}
+
+// VirtualDNSAnalyticsResponse represents a Virtual DNS analytics response.
+type VirtualDNSAnalyticsResponse struct {
+	Response
+	Result VirtualDNSAnalytics `json:"result"`
+}
+
+// CreateVirtualDNS creates a new Virtual DNS cluster.
+//
+// API reference: https://api.cloudflare.com/#virtual-dns-users--create-a-virtual-dns-cluster
+func (api *API) CreateVirtualDNS(v *VirtualDNS) (*VirtualDNS, error) {
+	res, err := api.makeRequest("POST", "/user/virtual_dns", v)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &VirtualDNSResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response.Result, nil
+}
+
+// VirtualDNS fetches a single virtual DNS cluster.
+//
+// API reference: https://api.cloudflare.com/#virtual-dns-users--get-a-virtual-dns-cluster
+func (api *API) VirtualDNS(virtualDNSID string) (*VirtualDNS, error) {
+	uri := "/user/virtual_dns/" + virtualDNSID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &VirtualDNSResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response.Result, nil
+}
+
+// ListVirtualDNS lists the virtual DNS clusters associated with an account.
+//
+// API reference: https://api.cloudflare.com/#virtual-dns-users--get-virtual-dns-clusters
+func (api *API) ListVirtualDNS() ([]*VirtualDNS, error) {
+	res, err := api.makeRequest("GET", "/user/virtual_dns", nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &VirtualDNSListResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response.Result, nil
+}
+
+// UpdateVirtualDNS updates a Virtual DNS cluster.
+//
+// API reference: https://api.cloudflare.com/#virtual-dns-users--modify-a-virtual-dns-cluster
+func (api *API) UpdateVirtualDNS(virtualDNSID string, vv VirtualDNS) error {
+	uri := "/user/virtual_dns/" + virtualDNSID
+	res, err := api.makeRequest("PUT", uri, vv)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &VirtualDNSResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+
+	return nil
+}
+
+// DeleteVirtualDNS deletes a Virtual DNS cluster. Note that this cannot be
+// undone, and will stop all traffic to that cluster.
+//
+// API reference: https://api.cloudflare.com/#virtual-dns-users--delete-a-virtual-dns-cluster
+func (api *API) DeleteVirtualDNS(virtualDNSID string) error {
+	uri := "/user/virtual_dns/" + virtualDNSID
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &VirtualDNSResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+
+	return nil
+}
+
+// encode encodes non-nil fields into URL encoded form.
+func (o VirtualDNSUserAnalyticsOptions) encode() string {
+	v := url.Values{}
+	if o.Since != nil {
+		v.Set("since", (*o.Since).UTC().Format(time.RFC3339))
+	}
+	if o.Until != nil {
+		v.Set("until", (*o.Until).UTC().Format(time.RFC3339))
+	}
+	if o.Metrics != nil {
+		v.Set("metrics", strings.Join(o.Metrics, ","))
+	}
+	return v.Encode()
+}
+
+// VirtualDNSUserAnalytics retrieves analytics report for a specified dimension and time range
+func (api *API) VirtualDNSUserAnalytics(virtualDNSID string, o VirtualDNSUserAnalyticsOptions) (VirtualDNSAnalytics, error) {
+	uri := "/user/virtual_dns/" + virtualDNSID + "/dns_analytics/report?" + o.encode()
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return VirtualDNSAnalytics{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := VirtualDNSAnalyticsResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return VirtualDNSAnalytics{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/waf.go b/vendor/github.com/cloudflare/cloudflare-go/waf.go
new file mode 100644
index 000000000..9b67f79a7
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/waf.go
@@ -0,0 +1,300 @@
+package cloudflare
+
+import (
+	"encoding/json"
+
+	"github.com/pkg/errors"
+)
+
+// WAFPackage represents a WAF package configuration.
+type WAFPackage struct {
+	ID            string `json:"id"`
+	Name          string `json:"name"`
+	Description   string `json:"description"`
+	ZoneID        string `json:"zone_id"`
+	DetectionMode string `json:"detection_mode"`
+	Sensitivity   string `json:"sensitivity"`
+	ActionMode    string `json:"action_mode"`
+}
+
+// WAFPackagesResponse represents the response from the WAF packages endpoint.
+type WAFPackagesResponse struct {
+	Response
+	Result     []WAFPackage `json:"result"`
+	ResultInfo ResultInfo   `json:"result_info"`
+}
+
+// WAFPackageResponse represents the response from the WAF package endpoint.
+type WAFPackageResponse struct {
+	Response
+	Result     WAFPackage `json:"result"`
+	ResultInfo ResultInfo `json:"result_info"`
+}
+
+// WAFPackageOptions represents options to edit a WAF package.
+type WAFPackageOptions struct {
+	Sensitivity string `json:"sensitivity,omitempty"`
+	ActionMode  string `json:"action_mode,omitempty"`
+}
+
+// WAFGroup represents a WAF rule group.
+type WAFGroup struct {
+	ID                 string   `json:"id"`
+	Name               string   `json:"name"`
+	Description        string   `json:"description"`
+	RulesCount         int      `json:"rules_count"`
+	ModifiedRulesCount int      `json:"modified_rules_count"`
+	PackageID          string   `json:"package_id"`
+	Mode               string   `json:"mode"`
+	AllowedModes       []string `json:"allowed_modes"`
+}
+
+// WAFGroupsResponse represents the response from the WAF groups endpoint.
+type WAFGroupsResponse struct {
+	Response
+	Result     []WAFGroup `json:"result"`
+	ResultInfo ResultInfo `json:"result_info"`
+}
+
+// WAFGroupResponse represents the response from the WAF group endpoint.
+type WAFGroupResponse struct {
+	Response
+	Result     WAFGroup   `json:"result"`
+	ResultInfo ResultInfo `json:"result_info"`
+}
+
+// WAFRule represents a WAF rule.
+type WAFRule struct {
+	ID          string `json:"id"`
+	Description string `json:"description"`
+	Priority    string `json:"priority"`
+	PackageID   string `json:"package_id"`
+	Group       struct {
+		ID   string `json:"id"`
+		Name string `json:"name"`
+	} `json:"group"`
+	Mode         string   `json:"mode"`
+	DefaultMode  string   `json:"default_mode"`
+	AllowedModes []string `json:"allowed_modes"`
+}
+
+// WAFRulesResponse represents the response from the WAF rules endpoint.
+type WAFRulesResponse struct {
+	Response
+	Result     []WAFRule  `json:"result"`
+	ResultInfo ResultInfo `json:"result_info"`
+}
+
+// WAFRuleResponse represents the response from the WAF rule endpoint.
+type WAFRuleResponse struct {
+	Response
+	Result     WAFRule    `json:"result"`
+	ResultInfo ResultInfo `json:"result_info"`
+}
+
+// WAFRuleOptions is a subset of WAFRule, for editable options.
+type WAFRuleOptions struct {
+	Mode string `json:"mode"`
+}
+
+// ListWAFPackages returns a slice of the WAF packages for the given zone.
+//
+// API Reference: https://api.cloudflare.com/#waf-rule-packages-list-firewall-packages
+func (api *API) ListWAFPackages(zoneID string) ([]WAFPackage, error) {
+	var p WAFPackagesResponse
+	var packages []WAFPackage
+	var res []byte
+	var err error
+	uri := "/zones/" + zoneID + "/firewall/waf/packages"
+	res, err = api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []WAFPackage{}, errors.Wrap(err, errMakeRequestError)
+	}
+	err = json.Unmarshal(res, &p)
+	if err != nil {
+		return []WAFPackage{}, errors.Wrap(err, errUnmarshalError)
+	}
+	if !p.Success {
+		// TODO: Provide an actual error message instead of always returning nil
+		return []WAFPackage{}, err
+	}
+	for pi := range p.Result {
+		packages = append(packages, p.Result[pi])
+	}
+	return packages, nil
+}
+
+// WAFPackage returns a WAF package for the given zone.
+//
+// API Reference: https://api.cloudflare.com/#waf-rule-packages-firewall-package-details
+func (api *API) WAFPackage(zoneID, packageID string) (WAFPackage, error) {
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return WAFPackage{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFPackageResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WAFPackage{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// UpdateWAFPackage lets you update the a WAF Package.
+//
+// API Reference: https://api.cloudflare.com/#waf-rule-packages-edit-firewall-package
+func (api *API) UpdateWAFPackage(zoneID, packageID string, opts WAFPackageOptions) (WAFPackage, error) {
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID
+	res, err := api.makeRequest("PATCH", uri, opts)
+	if err != nil {
+		return WAFPackage{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFPackageResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WAFPackage{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListWAFGroups returns a slice of the WAF groups for the given WAF package.
+//
+// API Reference: https://api.cloudflare.com/#waf-rule-groups-list-rule-groups
+func (api *API) ListWAFGroups(zoneID, packageID string) ([]WAFGroup, error) {
+	var groups []WAFGroup
+	var res []byte
+	var err error
+
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID + "/groups"
+	res, err = api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []WAFGroup{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFGroupsResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []WAFGroup{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	if !r.Success {
+		// TODO: Provide an actual error message instead of always returning nil
+		return []WAFGroup{}, err
+	}
+
+	for gi := range r.Result {
+		groups = append(groups, r.Result[gi])
+	}
+	return groups, nil
+}
+
+// WAFGroup returns a WAF rule group from the given WAF package.
+//
+// API Reference: https://api.cloudflare.com/#waf-rule-groups-rule-group-details
+func (api *API) WAFGroup(zoneID, packageID, groupID string) (WAFGroup, error) {
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID + "/groups/" + groupID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return WAFGroup{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFGroupResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WAFGroup{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// UpdateWAFGroup lets you update the mode of a WAF Group.
+//
+// API Reference: https://api.cloudflare.com/#waf-rule-groups-edit-rule-group
+func (api *API) UpdateWAFGroup(zoneID, packageID, groupID, mode string) (WAFGroup, error) {
+	opts := WAFRuleOptions{Mode: mode}
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID + "/groups/" + groupID
+	res, err := api.makeRequest("PATCH", uri, opts)
+	if err != nil {
+		return WAFGroup{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFGroupResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WAFGroup{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ListWAFRules returns a slice of the WAF rules for the given WAF package.
+//
+// API Reference: https://api.cloudflare.com/#waf-rules-list-rules
+func (api *API) ListWAFRules(zoneID, packageID string) ([]WAFRule, error) {
+	var rules []WAFRule
+	var res []byte
+	var err error
+
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID + "/rules"
+	res, err = api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []WAFRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFRulesResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []WAFRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	if !r.Success {
+		// TODO: Provide an actual error message instead of always returning nil
+		return []WAFRule{}, err
+	}
+
+	for ri := range r.Result {
+		rules = append(rules, r.Result[ri])
+	}
+	return rules, nil
+}
+
+// WAFRule returns a WAF rule from the given WAF package.
+//
+// API Reference: https://api.cloudflare.com/#waf-rules-rule-details
+func (api *API) WAFRule(zoneID, packageID, ruleID string) (WAFRule, error) {
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID + "/rules/" + ruleID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return WAFRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFRuleResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WAFRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// UpdateWAFRule lets you update the mode of a WAF Rule.
+//
+// API Reference: https://api.cloudflare.com/#waf-rules-edit-rule
+func (api *API) UpdateWAFRule(zoneID, packageID, ruleID, mode string) (WAFRule, error) {
+	opts := WAFRuleOptions{Mode: mode}
+	uri := "/zones/" + zoneID + "/firewall/waf/packages/" + packageID + "/rules/" + ruleID
+	res, err := api.makeRequest("PATCH", uri, opts)
+	if err != nil {
+		return WAFRule{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r WAFRuleResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WAFRule{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/workers.go b/vendor/github.com/cloudflare/cloudflare-go/workers.go
new file mode 100644
index 000000000..1ab795ec4
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/workers.go
@@ -0,0 +1,314 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/http"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// WorkerRequestParams provides parameters for worker requests for both enterprise and standard requests
+type WorkerRequestParams struct {
+	ZoneID     string
+	ScriptName string
+}
+
+// WorkerRoute aka filters are patterns used to enable or disable workers that match requests.
+//
+// API reference: https://api.cloudflare.com/#worker-filters-properties
+type WorkerRoute struct {
+	ID      string `json:"id,omitempty"`
+	Pattern string `json:"pattern"`
+	Enabled bool   `json:"enabled"`
+	Script  string `json:"script,omitempty"`
+}
+
+// WorkerRoutesResponse embeds Response struct and slice of WorkerRoutes
+type WorkerRoutesResponse struct {
+	Response
+	Routes []WorkerRoute `json:"result"`
+}
+
+// WorkerRouteResponse embeds Response struct and a single WorkerRoute
+type WorkerRouteResponse struct {
+	Response
+	WorkerRoute `json:"result"`
+}
+
+// WorkerScript Cloudflare Worker struct with metadata
+type WorkerScript struct {
+	WorkerMetaData
+	Script string `json:"script"`
+}
+
+// WorkerMetaData contains worker script information such as size, creation & modification dates
+type WorkerMetaData struct {
+	ID         string    `json:"id,omitempty"`
+	ETAG       string    `json:"etag,omitempty"`
+	Size       int       `json:"size,omitempty"`
+	CreatedOn  time.Time `json:"created_on,omitempty"`
+	ModifiedOn time.Time `json:"modified_on,omitempty"`
+}
+
+// WorkerListResponse wrapper struct for API response to worker script list API call
+type WorkerListResponse struct {
+	Response
+	WorkerList []WorkerMetaData `json:"result"`
+}
+
+// WorkerScriptResponse wrapper struct for API response to worker script calls
+type WorkerScriptResponse struct {
+	Response
+	WorkerScript `json:"result"`
+}
+
+// DeleteWorker deletes worker for a zone.
+//
+// API reference: https://api.cloudflare.com/#worker-script-delete-worker
+func (api *API) DeleteWorker(requestParams *WorkerRequestParams) (WorkerScriptResponse, error) {
+	// if ScriptName is provided we will treat as org request
+	if requestParams.ScriptName != "" {
+		return api.deleteWorkerWithName(requestParams.ScriptName)
+	}
+	uri := "/zones/" + requestParams.ZoneID + "/workers/script"
+	res, err := api.makeRequest("DELETE", uri, nil)
+	var r WorkerScriptResponse
+	if err != nil {
+		return r, errors.Wrap(err, errMakeRequestError)
+	}
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return r, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// DeleteWorkerWithName deletes worker for a zone.
+// This is an enterprise only feature https://developers.cloudflare.com/workers/api/config-api-for-enterprise
+// account must be specified as api option https://godoc.org/github.com/cloudflare/cloudflare-go#UsingAccount
+//
+// API reference: https://api.cloudflare.com/#worker-script-delete-worker
+func (api *API) deleteWorkerWithName(scriptName string) (WorkerScriptResponse, error) {
+	if api.AccountID == "" {
+		return WorkerScriptResponse{}, errors.New("account ID required for enterprise only request")
+	}
+	uri := "/accounts/" + api.AccountID + "/workers/scripts/" + scriptName
+	res, err := api.makeRequest("DELETE", uri, nil)
+	var r WorkerScriptResponse
+	if err != nil {
+		return r, errors.Wrap(err, errMakeRequestError)
+	}
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return r, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// DownloadWorker fetch raw script content for your worker returns []byte containing worker code js
+//
+// API reference: https://api.cloudflare.com/#worker-script-download-worker
+func (api *API) DownloadWorker(requestParams *WorkerRequestParams) (WorkerScriptResponse, error) {
+	if requestParams.ScriptName != "" {
+		return api.downloadWorkerWithName(requestParams.ScriptName)
+	}
+	uri := "/zones/" + requestParams.ZoneID + "/workers/script"
+	res, err := api.makeRequest("GET", uri, nil)
+	var r WorkerScriptResponse
+	if err != nil {
+		return r, errors.Wrap(err, errMakeRequestError)
+	}
+	r.Script = string(res)
+	r.Success = true
+	return r, nil
+}
+
+// DownloadWorkerWithName fetch raw script content for your worker returns string containing worker code js
+// This is an enterprise only feature https://developers.cloudflare.com/workers/api/config-api-for-enterprise/
+//
+// API reference: https://api.cloudflare.com/#worker-script-download-worker
+func (api *API) downloadWorkerWithName(scriptName string) (WorkerScriptResponse, error) {
+	if api.AccountID == "" {
+		return WorkerScriptResponse{}, errors.New("account ID required for enterprise only request")
+	}
+	uri := "/accounts/" + api.AccountID + "/workers/scripts/" + scriptName
+	res, err := api.makeRequest("GET", uri, nil)
+	var r WorkerScriptResponse
+	if err != nil {
+		return r, errors.Wrap(err, errMakeRequestError)
+	}
+	r.Script = string(res)
+	r.Success = true
+	return r, nil
+}
+
+// ListWorkerScripts returns list of worker scripts for given account.
+//
+// This is an enterprise only feature https://developers.cloudflare.com/workers/api/config-api-for-enterprise
+//
+// API reference: https://developers.cloudflare.com/workers/api/config-api-for-enterprise/
+func (api *API) ListWorkerScripts() (WorkerListResponse, error) {
+	if api.AccountID == "" {
+		return WorkerListResponse{}, errors.New("account ID required for enterprise only request")
+	}
+	uri := "/accounts/" + api.AccountID + "/workers/scripts"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return WorkerListResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r WorkerListResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WorkerListResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// UploadWorker push raw script content for your worker.
+//
+// API reference: https://api.cloudflare.com/#worker-script-upload-worker
+func (api *API) UploadWorker(requestParams *WorkerRequestParams, data string) (WorkerScriptResponse, error) {
+	if requestParams.ScriptName != "" {
+		return api.uploadWorkerWithName(requestParams.ScriptName, data)
+	}
+	uri := "/zones/" + requestParams.ZoneID + "/workers/script"
+	headers := make(http.Header)
+	headers.Set("Content-Type", "application/javascript")
+	res, err := api.makeRequestWithHeaders("PUT", uri, []byte(data), headers)
+	var r WorkerScriptResponse
+	if err != nil {
+		return r, errors.Wrap(err, errMakeRequestError)
+	}
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return r, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// UploadWorkerWithName push raw script content for your worker.
+//
+// This is an enterprise only feature https://developers.cloudflare.com/workers/api/config-api-for-enterprise/
+//
+// API reference: https://api.cloudflare.com/#worker-script-upload-worker
+func (api *API) uploadWorkerWithName(scriptName string, data string) (WorkerScriptResponse, error) {
+	if api.AccountID == "" {
+		return WorkerScriptResponse{}, errors.New("account ID required for enterprise only request")
+	}
+	uri := "/accounts/" + api.AccountID + "/workers/scripts/" + scriptName
+	headers := make(http.Header)
+	headers.Set("Content-Type", "application/javascript")
+	res, err := api.makeRequestWithHeaders("PUT", uri, []byte(data), headers)
+	var r WorkerScriptResponse
+	if err != nil {
+		return r, errors.Wrap(err, errMakeRequestError)
+	}
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return r, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// CreateWorkerRoute creates worker route for a zone
+//
+// API reference: https://api.cloudflare.com/#worker-filters-create-filter
+func (api *API) CreateWorkerRoute(zoneID string, route WorkerRoute) (WorkerRouteResponse, error) {
+	// Check whether a script name is defined in order to determine whether
+	// to use the single-script or multi-script endpoint.
+	pathComponent := "filters"
+	if route.Script != "" {
+		if api.AccountID == "" {
+			return WorkerRouteResponse{}, errors.New("account ID required for enterprise only request")
+		}
+		pathComponent = "routes"
+	}
+
+	uri := "/zones/" + zoneID + "/workers/" + pathComponent
+	res, err := api.makeRequest("POST", uri, route)
+	if err != nil {
+		return WorkerRouteResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r WorkerRouteResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WorkerRouteResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// DeleteWorkerRoute deletes worker route for a zone
+//
+// API reference: https://api.cloudflare.com/#worker-filters-delete-filter
+func (api *API) DeleteWorkerRoute(zoneID string, routeID string) (WorkerRouteResponse, error) {
+	// For deleting a route, it doesn't matter whether we use the
+	// single-script or multi-script endpoint
+	uri := "/zones/" + zoneID + "/workers/filters/" + routeID
+	res, err := api.makeRequest("DELETE", uri, nil)
+	if err != nil {
+		return WorkerRouteResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r WorkerRouteResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WorkerRouteResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// ListWorkerRoutes returns list of worker routes
+//
+// API reference: https://api.cloudflare.com/#worker-filters-list-filters
+func (api *API) ListWorkerRoutes(zoneID string) (WorkerRoutesResponse, error) {
+	pathComponent := "filters"
+	if api.AccountID != "" {
+		pathComponent = "routes"
+	}
+	uri := "/zones/" + zoneID + "/workers/" + pathComponent
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return WorkerRoutesResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r WorkerRoutesResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WorkerRoutesResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+	for i := range r.Routes {
+		route := &r.Routes[i]
+		// The Enabled flag will not be set in the multi-script API response
+		// so we manually set it to true if the script name is not empty
+		// in case any multi-script customers rely on the Enabled field
+		if route.Script != "" {
+			route.Enabled = true
+		}
+	}
+	return r, nil
+}
+
+// UpdateWorkerRoute updates worker route for a zone.
+//
+// API reference: https://api.cloudflare.com/#worker-filters-update-filter
+func (api *API) UpdateWorkerRoute(zoneID string, routeID string, route WorkerRoute) (WorkerRouteResponse, error) {
+	// Check whether a script name is defined in order to determine whether
+	// to use the single-script or multi-script endpoint.
+	pathComponent := "filters"
+	if route.Script != "" {
+		if api.AccountID == "" {
+			return WorkerRouteResponse{}, errors.New("account ID required for enterprise only request")
+		}
+		pathComponent = "routes"
+	}
+	uri := "/zones/" + zoneID + "/workers/" + pathComponent + "/" + routeID
+	res, err := api.makeRequest("PUT", uri, route)
+	if err != nil {
+		return WorkerRouteResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r WorkerRouteResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return WorkerRouteResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/workers_kv.go b/vendor/github.com/cloudflare/cloudflare-go/workers_kv.go
new file mode 100644
index 000000000..92197af08
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/workers_kv.go
@@ -0,0 +1,192 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/pkg/errors"
+)
+
+// WorkersKVNamespaceRequest provides parameters for creating and updating storage namespaces
+type WorkersKVNamespaceRequest struct {
+	Title string `json:"title"`
+}
+
+// WorkersKVNamespaceResponse is the response received when creating storage namespaces
+type WorkersKVNamespaceResponse struct {
+	Response
+	Result WorkersKVNamespace `json:"result"`
+}
+
+// WorkersKVNamespace contains the unique identifier and title of a storage namespace
+type WorkersKVNamespace struct {
+	ID    string `json:"id"`
+	Title string `json:"title"`
+}
+
+// ListWorkersKVNamespacesResponse contains a slice of storage namespaces associated with an
+// account, pagination information, and an embedded response struct
+type ListWorkersKVNamespacesResponse struct {
+	Response
+	Result     []WorkersKVNamespace `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// StorageKey is a key name used to identify a storage value
+type StorageKey struct {
+	Name string `json:"name"`
+}
+
+// ListStorageKeysResponse contains a slice of keys belonging to a storage namespace,
+// pagination information, and an embedded response struct
+type ListStorageKeysResponse struct {
+	Response
+	Result     []StorageKey `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// CreateWorkersKVNamespace creates a namespace under the given title.
+// A 400 is returned if the account already owns a namespace with this title.
+// A namespace must be explicitly deleted to be replaced.
+//
+// API reference: https://api.cloudflare.com/#workers-kv-namespace-create-a-namespace
+func (api *API) CreateWorkersKVNamespace(ctx context.Context, req *WorkersKVNamespaceRequest) (WorkersKVNamespaceResponse, error) {
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces", api.AccountID)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, req)
+	if err != nil {
+		return WorkersKVNamespaceResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := WorkersKVNamespaceResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return result, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result, err
+}
+
+// ListWorkersKVNamespaces lists storage namespaces
+//
+// API reference: https://api.cloudflare.com/#workers-kv-namespace-list-namespaces
+func (api *API) ListWorkersKVNamespaces(ctx context.Context) (ListWorkersKVNamespacesResponse, error) {
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces", api.AccountID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return ListWorkersKVNamespacesResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := ListWorkersKVNamespacesResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return result, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result, err
+}
+
+// DeleteWorkersKVNamespace deletes the namespace corresponding to the given ID
+//
+// API reference: https://api.cloudflare.com/#workers-kv-namespace-remove-a-namespace
+func (api *API) DeleteWorkersKVNamespace(ctx context.Context, namespaceID string) (Response, error) {
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s", api.AccountID, namespaceID)
+	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return Response{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := Response{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return result, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result, err
+}
+
+// UpdateWorkersKVNamespace modifies a namespace's title
+//
+// API reference: https://api.cloudflare.com/#workers-kv-namespace-rename-a-namespace
+func (api *API) UpdateWorkersKVNamespace(ctx context.Context, namespaceID string, req *WorkersKVNamespaceRequest) (Response, error) {
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s", api.AccountID, namespaceID)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, req)
+	if err != nil {
+		return Response{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := Response{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return result, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result, err
+}
+
+// WriteWorkersKV writes a value identified by a key.
+//
+// API reference: https://api.cloudflare.com/#workers-kv-namespace-write-key-value-pair
+func (api *API) WriteWorkersKV(ctx context.Context, namespaceID, key string, value []byte) (Response, error) {
+	key = url.PathEscape(key)
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/values/%s", api.AccountID, namespaceID, key)
+	res, err := api.makeRequestWithAuthTypeAndHeaders(
+		ctx, http.MethodPut, uri, value, api.authType, http.Header{"Content-Type": []string{"application/octet-stream"}},
+	)
+	if err != nil {
+		return Response{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := Response{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return result, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return result, err
+}
+
+// ReadWorkersKV returns the value associated with the given key in the given namespace
+//
+// API reference: https://api.cloudflare.com/#workers-kv-namespace-read-key-value-pair
+func (api API) ReadWorkersKV(ctx context.Context, namespaceID, key string) ([]byte, error) {
+	key = url.PathEscape(key)
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/values/%s", api.AccountID, namespaceID, key)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	return res, nil
+}
+
+// DeleteWorkersKV deletes a key and value for a provided storage namespace
+//
+// API reference: https://api.cloudflare.com/#workers-kv-namespace-delete-key-value-pair
+func (api API) DeleteWorkersKV(ctx context.Context, namespaceID, key string) (Response, error) {
+	key = url.PathEscape(key)
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/values/%s", api.AccountID, namespaceID, key)
+	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return Response{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := Response{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return result, errors.Wrap(err, errUnmarshalError)
+	}
+	return result, err
+}
+
+// ListWorkersKVs lists a namespace's keys
+//
+// API Reference: https://api.cloudflare.com/#workers-kv-namespace-list-a-namespace-s-keys
+func (api API) ListWorkersKVs(ctx context.Context, namespaceID string) (ListStorageKeysResponse, error) {
+	uri := fmt.Sprintf("/accounts/%s/storage/kv/namespaces/%s/keys", api.AccountID, namespaceID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return ListStorageKeysResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	result := ListStorageKeysResponse{}
+	if err := json.Unmarshal(res, &result); err != nil {
+		return result, errors.Wrap(err, errUnmarshalError)
+	}
+	return result, err
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/zone.go b/vendor/github.com/cloudflare/cloudflare-go/zone.go
new file mode 100644
index 000000000..28f54a5da
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/zone.go
@@ -0,0 +1,740 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"sync"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// Owner describes the resource owner.
+type Owner struct {
+	ID        string `json:"id"`
+	Email     string `json:"email"`
+	Name      string `json:"name"`
+	OwnerType string `json:"type"`
+}
+
+// Zone describes a Cloudflare zone.
+type Zone struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+	// DevMode contains the time in seconds until development expires (if
+	// positive) or since it expired (if negative). It will be 0 if never used.
+	DevMode           int       `json:"development_mode"`
+	OriginalNS        []string  `json:"original_name_servers"`
+	OriginalRegistrar string    `json:"original_registrar"`
+	OriginalDNSHost   string    `json:"original_dnshost"`
+	CreatedOn         time.Time `json:"created_on"`
+	ModifiedOn        time.Time `json:"modified_on"`
+	NameServers       []string  `json:"name_servers"`
+	Owner             Owner     `json:"owner"`
+	Permissions       []string  `json:"permissions"`
+	Plan              ZonePlan  `json:"plan"`
+	PlanPending       ZonePlan  `json:"plan_pending,omitempty"`
+	Status            string    `json:"status"`
+	Paused            bool      `json:"paused"`
+	Type              string    `json:"type"`
+	Host              struct {
+		Name    string
+		Website string
+	} `json:"host"`
+	VanityNS    []string `json:"vanity_name_servers"`
+	Betas       []string `json:"betas"`
+	DeactReason string   `json:"deactivation_reason"`
+	Meta        ZoneMeta `json:"meta"`
+	Account     Account  `json:"account"`
+}
+
+// ZoneMeta describes metadata about a zone.
+type ZoneMeta struct {
+	// custom_certificate_quota is broken - sometimes it's a string, sometimes a number!
+	// CustCertQuota     int    `json:"custom_certificate_quota"`
+	PageRuleQuota     int  `json:"page_rule_quota"`
+	WildcardProxiable bool `json:"wildcard_proxiable"`
+	PhishingDetected  bool `json:"phishing_detected"`
+}
+
+// ZonePlan contains the plan information for a zone.
+type ZonePlan struct {
+	ZonePlanCommon
+	IsSubscribed      bool   `json:"is_subscribed"`
+	CanSubscribe      bool   `json:"can_subscribe"`
+	LegacyID          string `json:"legacy_id"`
+	LegacyDiscount    bool   `json:"legacy_discount"`
+	ExternallyManaged bool   `json:"externally_managed"`
+}
+
+// ZoneRatePlan contains the plan information for a zone.
+type ZoneRatePlan struct {
+	ZonePlanCommon
+	Components []zoneRatePlanComponents `json:"components,omitempty"`
+}
+
+// ZonePlanCommon contains fields used by various Plan endpoints
+type ZonePlanCommon struct {
+	ID        string `json:"id"`
+	Name      string `json:"name,omitempty"`
+	Price     int    `json:"price,omitempty"`
+	Currency  string `json:"currency,omitempty"`
+	Frequency string `json:"frequency,omitempty"`
+}
+
+type zoneRatePlanComponents struct {
+	Name      string `json:"name"`
+	Default   int    `json:"Default"`
+	UnitPrice int    `json:"unit_price"`
+}
+
+// ZoneID contains only the zone ID.
+type ZoneID struct {
+	ID string `json:"id"`
+}
+
+// ZoneResponse represents the response from the Zone endpoint containing a single zone.
+type ZoneResponse struct {
+	Response
+	Result Zone `json:"result"`
+}
+
+// ZonesResponse represents the response from the Zone endpoint containing an array of zones.
+type ZonesResponse struct {
+	Response
+	Result     []Zone `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// ZoneIDResponse represents the response from the Zone endpoint, containing only a zone ID.
+type ZoneIDResponse struct {
+	Response
+	Result ZoneID `json:"result"`
+}
+
+// AvailableZoneRatePlansResponse represents the response from the Available Rate Plans endpoint.
+type AvailableZoneRatePlansResponse struct {
+	Response
+	Result     []ZoneRatePlan `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// AvailableZonePlansResponse represents the response from the Available Plans endpoint.
+type AvailableZonePlansResponse struct {
+	Response
+	Result []ZonePlan `json:"result"`
+	ResultInfo
+}
+
+// ZoneRatePlanResponse represents the response from the Plan Details endpoint.
+type ZoneRatePlanResponse struct {
+	Response
+	Result ZoneRatePlan `json:"result"`
+}
+
+// ZoneSetting contains settings for a zone.
+type ZoneSetting struct {
+	ID            string      `json:"id"`
+	Editable      bool        `json:"editable"`
+	ModifiedOn    string      `json:"modified_on"`
+	Value         interface{} `json:"value"`
+	TimeRemaining int         `json:"time_remaining"`
+}
+
+// ZoneSettingResponse represents the response from the Zone Setting endpoint.
+type ZoneSettingResponse struct {
+	Response
+	Result []ZoneSetting `json:"result"`
+}
+
+// ZoneSSLSetting contains ssl setting for a zone.
+type ZoneSSLSetting struct {
+	ID                string `json:"id"`
+	Editable          bool   `json:"editable"`
+	ModifiedOn        string `json:"modified_on"`
+	Value             string `json:"value"`
+	CertificateStatus string `json:"certificate_status"`
+}
+
+// ZoneSSLSettingResponse represents the response from the Zone SSL Setting
+// endpoint.
+type ZoneSSLSettingResponse struct {
+	Response
+	Result ZoneSSLSetting `json:"result"`
+}
+
+// ZoneAnalyticsData contains totals and timeseries analytics data for a zone.
+type ZoneAnalyticsData struct {
+	Totals     ZoneAnalytics   `json:"totals"`
+	Timeseries []ZoneAnalytics `json:"timeseries"`
+}
+
+// zoneAnalyticsDataResponse represents the response from the Zone Analytics Dashboard endpoint.
+type zoneAnalyticsDataResponse struct {
+	Response
+	Result ZoneAnalyticsData `json:"result"`
+}
+
+// ZoneAnalyticsColocation contains analytics data by datacenter.
+type ZoneAnalyticsColocation struct {
+	ColocationID string          `json:"colo_id"`
+	Timeseries   []ZoneAnalytics `json:"timeseries"`
+}
+
+// zoneAnalyticsColocationResponse represents the response from the Zone Analytics By Co-location endpoint.
+type zoneAnalyticsColocationResponse struct {
+	Response
+	Result []ZoneAnalyticsColocation `json:"result"`
+}
+
+// ZoneAnalytics contains analytics data for a zone.
+type ZoneAnalytics struct {
+	Since    time.Time `json:"since"`
+	Until    time.Time `json:"until"`
+	Requests struct {
+		All         int            `json:"all"`
+		Cached      int            `json:"cached"`
+		Uncached    int            `json:"uncached"`
+		ContentType map[string]int `json:"content_type"`
+		Country     map[string]int `json:"country"`
+		SSL         struct {
+			Encrypted   int `json:"encrypted"`
+			Unencrypted int `json:"unencrypted"`
+		} `json:"ssl"`
+		HTTPStatus map[string]int `json:"http_status"`
+	} `json:"requests"`
+	Bandwidth struct {
+		All         int            `json:"all"`
+		Cached      int            `json:"cached"`
+		Uncached    int            `json:"uncached"`
+		ContentType map[string]int `json:"content_type"`
+		Country     map[string]int `json:"country"`
+		SSL         struct {
+			Encrypted   int `json:"encrypted"`
+			Unencrypted int `json:"unencrypted"`
+		} `json:"ssl"`
+	} `json:"bandwidth"`
+	Threats struct {
+		All     int            `json:"all"`
+		Country map[string]int `json:"country"`
+		Type    map[string]int `json:"type"`
+	} `json:"threats"`
+	Pageviews struct {
+		All           int            `json:"all"`
+		SearchEngines map[string]int `json:"search_engines"`
+	} `json:"pageviews"`
+	Uniques struct {
+		All int `json:"all"`
+	}
+}
+
+// ZoneAnalyticsOptions represents the optional parameters in Zone Analytics
+// endpoint requests.
+type ZoneAnalyticsOptions struct {
+	Since      *time.Time
+	Until      *time.Time
+	Continuous *bool
+}
+
+// PurgeCacheRequest represents the request format made to the purge endpoint.
+type PurgeCacheRequest struct {
+	Everything bool `json:"purge_everything,omitempty"`
+	// Purge by filepath (exact match). Limit of 30
+	Files []string `json:"files,omitempty"`
+	// Purge by Tag (Enterprise only):
+	// https://support.cloudflare.com/hc/en-us/articles/206596608-How-to-Purge-Cache-Using-Cache-Tags-Enterprise-only-
+	Tags []string `json:"tags,omitempty"`
+	// Purge by hostname - e.g. "assets.example.com"
+	Hosts []string `json:"hosts,omitempty"`
+}
+
+// PurgeCacheResponse represents the response from the purge endpoint.
+type PurgeCacheResponse struct {
+	Response
+	Result struct {
+		ID string `json:"id"`
+	} `json:"result"`
+}
+
+// newZone describes a new zone.
+type newZone struct {
+	Name      string `json:"name"`
+	JumpStart bool   `json:"jump_start"`
+	Type      string `json:"type"`
+	// We use a pointer to get a nil type when the field is empty.
+	// This allows us to completely omit this with json.Marshal().
+	Account *Account `json:"organization,omitempty"`
+}
+
+// FallbackOrigin describes a fallback origin
+type FallbackOrigin struct {
+	Value string `json:"value"`
+	ID    string `json:"id,omitempty"`
+}
+
+// FallbackOriginResponse represents the response from the fallback_origin endpoint
+type FallbackOriginResponse struct {
+	Response
+	Result FallbackOrigin `json:"result"`
+}
+
+// CreateZone creates a zone on an account.
+//
+// Setting jumpstart to true will attempt to automatically scan for existing
+// DNS records. Setting this to false will create the zone with no DNS records.
+//
+// If account is non-empty, it must have at least the ID field populated.
+// This will add the new zone to the specified multi-user account.
+//
+// API reference: https://api.cloudflare.com/#zone-create-a-zone
+func (api *API) CreateZone(name string, jumpstart bool, account Account, zoneType string) (Zone, error) {
+	var newzone newZone
+	newzone.Name = name
+	newzone.JumpStart = jumpstart
+	if account.ID != "" {
+		newzone.Account = &account
+	}
+
+	if zoneType == "partial" {
+		newzone.Type = "partial"
+	} else {
+		newzone.Type = "full"
+	}
+
+	res, err := api.makeRequest("POST", "/zones", newzone)
+	if err != nil {
+		return Zone{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r ZoneResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return Zone{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ZoneActivationCheck initiates another zone activation check for newly-created zones.
+//
+// API reference: https://api.cloudflare.com/#zone-initiate-another-zone-activation-check
+func (api *API) ZoneActivationCheck(zoneID string) (Response, error) {
+	res, err := api.makeRequest("PUT", "/zones/"+zoneID+"/activation_check", nil)
+	if err != nil {
+		return Response{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r Response
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return Response{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// ListZones lists zones on an account. Optionally takes a list of zone names
+// to filter against.
+//
+// API reference: https://api.cloudflare.com/#zone-list-zones
+func (api *API) ListZones(z ...string) ([]Zone, error) {
+	v := url.Values{}
+	var res []byte
+	var r ZonesResponse
+	var zones []Zone
+	var err error
+	if len(z) > 0 {
+		for _, zone := range z {
+			v.Set("name", zone)
+			res, err = api.makeRequest("GET", "/zones?"+v.Encode(), nil)
+			if err != nil {
+				return []Zone{}, errors.Wrap(err, errMakeRequestError)
+			}
+			err = json.Unmarshal(res, &r)
+			if err != nil {
+				return []Zone{}, errors.Wrap(err, errUnmarshalError)
+			}
+			if !r.Success {
+				// TODO: Provide an actual error message instead of always returning nil
+				return []Zone{}, err
+			}
+			for zi := range r.Result {
+				zones = append(zones, r.Result[zi])
+			}
+		}
+	} else {
+		res, err = api.makeRequest("GET", "/zones?per_page=50", nil)
+		if err != nil {
+			return []Zone{}, errors.Wrap(err, errMakeRequestError)
+		}
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return []Zone{}, errors.Wrap(err, errUnmarshalError)
+		}
+
+		totalPageCount := r.TotalPages
+		var wg sync.WaitGroup
+		wg.Add(totalPageCount)
+		errc := make(chan error)
+
+		for i := 1; i <= totalPageCount; i++ {
+			go func(pageNumber int) error {
+				res, err = api.makeRequest("GET", fmt.Sprintf("/zones?per_page=50&page=%d", pageNumber), nil)
+				if err != nil {
+					errc <- err
+				}
+
+				err = json.Unmarshal(res, &r)
+				if err != nil {
+					errc <- err
+				}
+
+				for _, zone := range r.Result {
+					zones = append(zones, zone)
+				}
+
+				select {
+				case err := <-errc:
+					return err
+				default:
+					wg.Done()
+				}
+
+				return nil
+			}(i)
+		}
+
+		wg.Wait()
+	}
+
+	return zones, nil
+}
+
+// ListZonesContext lists zones on an account. Optionally takes a list of ReqOptions.
+func (api *API) ListZonesContext(ctx context.Context, opts ...ReqOption) (r ZonesResponse, err error) {
+	var res []byte
+	opt := reqOption{
+		params: url.Values{},
+	}
+	for _, of := range opts {
+		of(&opt)
+	}
+
+	res, err = api.makeRequestContext(ctx, "GET", "/zones?"+opt.params.Encode(), nil)
+	if err != nil {
+		return ZonesResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return ZonesResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r, nil
+}
+
+// ZoneDetails fetches information about a zone.
+//
+// API reference: https://api.cloudflare.com/#zone-zone-details
+func (api *API) ZoneDetails(zoneID string) (Zone, error) {
+	res, err := api.makeRequest("GET", "/zones/"+zoneID, nil)
+	if err != nil {
+		return Zone{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r ZoneResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return Zone{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ZoneOptions is a subset of Zone, for editable options.
+type ZoneOptions struct {
+	Paused   *bool     `json:"paused,omitempty"`
+	VanityNS []string  `json:"vanity_name_servers,omitempty"`
+	Plan     *ZonePlan `json:"plan,omitempty"`
+}
+
+// ZoneSetPaused pauses Cloudflare service for the entire zone, sending all
+// traffic direct to the origin.
+func (api *API) ZoneSetPaused(zoneID string, paused bool) (Zone, error) {
+	zoneopts := ZoneOptions{Paused: &paused}
+	zone, err := api.EditZone(zoneID, zoneopts)
+	if err != nil {
+		return Zone{}, err
+	}
+
+	return zone, nil
+}
+
+// ZoneSetVanityNS sets custom nameservers for the zone.
+// These names must be within the same zone.
+func (api *API) ZoneSetVanityNS(zoneID string, ns []string) (Zone, error) {
+	zoneopts := ZoneOptions{VanityNS: ns}
+	zone, err := api.EditZone(zoneID, zoneopts)
+	if err != nil {
+		return Zone{}, err
+	}
+
+	return zone, nil
+}
+
+// ZoneSetPlan changes the zone plan.
+func (api *API) ZoneSetPlan(zoneID string, plan ZonePlan) (Zone, error) {
+	zoneopts := ZoneOptions{Plan: &plan}
+	zone, err := api.EditZone(zoneID, zoneopts)
+	if err != nil {
+		return Zone{}, err
+	}
+
+	return zone, nil
+}
+
+// EditZone edits the given zone.
+//
+// This is usually called by ZoneSetPaused, ZoneSetVanityNS or ZoneSetPlan.
+//
+// API reference: https://api.cloudflare.com/#zone-edit-zone-properties
+func (api *API) EditZone(zoneID string, zoneOpts ZoneOptions) (Zone, error) {
+	res, err := api.makeRequest("PATCH", "/zones/"+zoneID, zoneOpts)
+	if err != nil {
+		return Zone{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r ZoneResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return Zone{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// PurgeEverything purges the cache for the given zone.
+//
+// Note: this will substantially increase load on the origin server for that
+// zone if there is a high cached vs. uncached request ratio.
+//
+// API reference: https://api.cloudflare.com/#zone-purge-all-files
+func (api *API) PurgeEverything(zoneID string) (PurgeCacheResponse, error) {
+	uri := "/zones/" + zoneID + "/purge_cache"
+	res, err := api.makeRequest("POST", uri, PurgeCacheRequest{true, nil, nil, nil})
+	if err != nil {
+		return PurgeCacheResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r PurgeCacheResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return PurgeCacheResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// PurgeCache purges the cache using the given PurgeCacheRequest (zone/url/tag).
+//
+// API reference: https://api.cloudflare.com/#zone-purge-individual-files-by-url-and-cache-tags
+func (api *API) PurgeCache(zoneID string, pcr PurgeCacheRequest) (PurgeCacheResponse, error) {
+	uri := "/zones/" + zoneID + "/purge_cache"
+	res, err := api.makeRequest("POST", uri, pcr)
+	if err != nil {
+		return PurgeCacheResponse{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r PurgeCacheResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return PurgeCacheResponse{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r, nil
+}
+
+// DeleteZone deletes the given zone.
+//
+// API reference: https://api.cloudflare.com/#zone-delete-a-zone
+func (api *API) DeleteZone(zoneID string) (ZoneID, error) {
+	res, err := api.makeRequest("DELETE", "/zones/"+zoneID, nil)
+	if err != nil {
+		return ZoneID{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r ZoneIDResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return ZoneID{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// AvailableZoneRatePlans returns information about all plans available to the specified zone.
+//
+// API reference: https://api.cloudflare.com/#zone-plan-available-plans
+func (api *API) AvailableZoneRatePlans(zoneID string) ([]ZoneRatePlan, error) {
+	uri := "/zones/" + zoneID + "/available_rate_plans"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []ZoneRatePlan{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r AvailableZoneRatePlansResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []ZoneRatePlan{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// AvailableZonePlans returns information about all plans available to the specified zone.
+//
+// API reference: https://api.cloudflare.com/#zone-rate-plan-list-available-plans
+func (api *API) AvailableZonePlans(zoneID string) ([]ZonePlan, error) {
+	uri := "/zones/" + zoneID + "/available_plans"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []ZonePlan{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r AvailableZonePlansResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []ZonePlan{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// encode encodes non-nil fields into URL encoded form.
+func (o ZoneAnalyticsOptions) encode() string {
+	v := url.Values{}
+	if o.Since != nil {
+		v.Set("since", (*o.Since).Format(time.RFC3339))
+	}
+	if o.Until != nil {
+		v.Set("until", (*o.Until).Format(time.RFC3339))
+	}
+	if o.Continuous != nil {
+		v.Set("continuous", fmt.Sprintf("%t", *o.Continuous))
+	}
+	return v.Encode()
+}
+
+// ZoneAnalyticsDashboard returns zone analytics information.
+//
+// API reference: https://api.cloudflare.com/#zone-analytics-dashboard
+func (api *API) ZoneAnalyticsDashboard(zoneID string, options ZoneAnalyticsOptions) (ZoneAnalyticsData, error) {
+	uri := "/zones/" + zoneID + "/analytics/dashboard" + "?" + options.encode()
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return ZoneAnalyticsData{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneAnalyticsDataResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return ZoneAnalyticsData{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ZoneAnalyticsByColocation returns zone analytics information by datacenter.
+//
+// API reference: https://api.cloudflare.com/#zone-analytics-analytics-by-co-locations
+func (api *API) ZoneAnalyticsByColocation(zoneID string, options ZoneAnalyticsOptions) ([]ZoneAnalyticsColocation, error) {
+	uri := "/zones/" + zoneID + "/analytics/colos" + "?" + options.encode()
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+	var r zoneAnalyticsColocationResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// ZoneSettings returns all of the settings for a given zone.
+//
+// API reference: https://api.cloudflare.com/#zone-settings-get-all-zone-settings
+func (api *API) ZoneSettings(zoneID string) (*ZoneSettingResponse, error) {
+	uri := "/zones/" + zoneID + "/settings"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &ZoneSettingResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// UpdateZoneSettings updates the settings for a given zone.
+//
+// API reference: https://api.cloudflare.com/#zone-settings-edit-zone-settings-info
+func (api *API) UpdateZoneSettings(zoneID string, settings []ZoneSetting) (*ZoneSettingResponse, error) {
+	uri := "/zones/" + zoneID + "/settings"
+	res, err := api.makeRequest("PATCH", uri, struct {
+		Items []ZoneSetting `json:"items"`
+	}{settings})
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &ZoneSettingResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
+
+// ZoneSSLSettings returns information about SSL setting to the specified zone.
+//
+// API reference: https://api.cloudflare.com/#zone-settings-get-ssl-setting
+func (api *API) ZoneSSLSettings(zoneID string) (ZoneSSLSetting, error) {
+	uri := "/zones/" + zoneID + "/settings/ssl"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return ZoneSSLSetting{}, errors.Wrap(err, errMakeRequestError)
+	}
+	var r ZoneSSLSettingResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return ZoneSSLSetting{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return r.Result, nil
+}
+
+// FallbackOrigin returns information about the fallback origin for the specified zone.
+//
+// API reference: https://developers.cloudflare.com/ssl/ssl-for-saas/api-calls/#fallback-origin-configuration
+func (api *API) FallbackOrigin(zoneID string) (FallbackOrigin, error) {
+	uri := "/zones/" + zoneID + "/fallback_origin"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return FallbackOrigin{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var r FallbackOriginResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return FallbackOrigin{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// UpdateFallbackOrigin updates the fallback origin for a given zone.
+//
+// API reference: https://developers.cloudflare.com/ssl/ssl-for-saas/api-calls/#4-example-patch-to-change-fallback-origin
+func (api *API) UpdateFallbackOrigin(zoneID string, fbo FallbackOrigin) (*FallbackOriginResponse, error) {
+	uri := "/zones/" + zoneID + "/fallback_origin"
+	res, err := api.makeRequest("PATCH", uri, fbo)
+	if err != nil {
+		return nil, errors.Wrap(err, errMakeRequestError)
+	}
+
+	response := &FallbackOriginResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return nil, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response, nil
+}
diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE
new file mode 100644
index 000000000..6a66aea5e
--- /dev/null
+++ b/vendor/golang.org/x/time/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/golang.org/x/time/PATENTS b/vendor/golang.org/x/time/PATENTS
new file mode 100644
index 000000000..733099041
--- /dev/null
+++ b/vendor/golang.org/x/time/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Go project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Go, where such license applies only to those patent
+claims, both currently owned or controlled by Google and acquired in
+the future, licensable by Google that are necessarily infringed by this
+implementation of Go.  This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation.  If you or your agent or exclusive licensee institute or
+order or agree to the institution of patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that this implementation of Go or any code incorporated within this
+implementation of Go constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of Go
+shall terminate as of the date such litigation is filed.
diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go
new file mode 100644
index 000000000..ae93e2471
--- /dev/null
+++ b/vendor/golang.org/x/time/rate/rate.go
@@ -0,0 +1,374 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package rate provides a rate limiter.
+package rate
+
+import (
+	"context"
+	"fmt"
+	"math"
+	"sync"
+	"time"
+)
+
+// Limit defines the maximum frequency of some events.
+// Limit is represented as number of events per second.
+// A zero Limit allows no events.
+type Limit float64
+
+// Inf is the infinite rate limit; it allows all events (even if burst is zero).
+const Inf = Limit(math.MaxFloat64)
+
+// Every converts a minimum time interval between events to a Limit.
+func Every(interval time.Duration) Limit {
+	if interval <= 0 {
+		return Inf
+	}
+	return 1 / Limit(interval.Seconds())
+}
+
+// A Limiter controls how frequently events are allowed to happen.
+// It implements a "token bucket" of size b, initially full and refilled
+// at rate r tokens per second.
+// Informally, in any large enough time interval, the Limiter limits the
+// rate to r tokens per second, with a maximum burst size of b events.
+// As a special case, if r == Inf (the infinite rate), b is ignored.
+// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
+//
+// The zero value is a valid Limiter, but it will reject all events.
+// Use NewLimiter to create non-zero Limiters.
+//
+// Limiter has three main methods, Allow, Reserve, and Wait.
+// Most callers should use Wait.
+//
+// Each of the three methods consumes a single token.
+// They differ in their behavior when no token is available.
+// If no token is available, Allow returns false.
+// If no token is available, Reserve returns a reservation for a future token
+// and the amount of time the caller must wait before using it.
+// If no token is available, Wait blocks until one can be obtained
+// or its associated context.Context is canceled.
+//
+// The methods AllowN, ReserveN, and WaitN consume n tokens.
+type Limiter struct {
+	limit Limit
+	burst int
+
+	mu     sync.Mutex
+	tokens float64
+	// last is the last time the limiter's tokens field was updated
+	last time.Time
+	// lastEvent is the latest time of a rate-limited event (past or future)
+	lastEvent time.Time
+}
+
+// Limit returns the maximum overall event rate.
+func (lim *Limiter) Limit() Limit {
+	lim.mu.Lock()
+	defer lim.mu.Unlock()
+	return lim.limit
+}
+
+// Burst returns the maximum burst size. Burst is the maximum number of tokens
+// that can be consumed in a single call to Allow, Reserve, or Wait, so higher
+// Burst values allow more events to happen at once.
+// A zero Burst allows no events, unless limit == Inf.
+func (lim *Limiter) Burst() int {
+	return lim.burst
+}
+
+// NewLimiter returns a new Limiter that allows events up to rate r and permits
+// bursts of at most b tokens.
+func NewLimiter(r Limit, b int) *Limiter {
+	return &Limiter{
+		limit: r,
+		burst: b,
+	}
+}
+
+// Allow is shorthand for AllowN(time.Now(), 1).
+func (lim *Limiter) Allow() bool {
+	return lim.AllowN(time.Now(), 1)
+}
+
+// AllowN reports whether n events may happen at time now.
+// Use this method if you intend to drop / skip events that exceed the rate limit.
+// Otherwise use Reserve or Wait.
+func (lim *Limiter) AllowN(now time.Time, n int) bool {
+	return lim.reserveN(now, n, 0).ok
+}
+
+// A Reservation holds information about events that are permitted by a Limiter to happen after a delay.
+// A Reservation may be canceled, which may enable the Limiter to permit additional events.
+type Reservation struct {
+	ok        bool
+	lim       *Limiter
+	tokens    int
+	timeToAct time.Time
+	// This is the Limit at reservation time, it can change later.
+	limit Limit
+}
+
+// OK returns whether the limiter can provide the requested number of tokens
+// within the maximum wait time.  If OK is false, Delay returns InfDuration, and
+// Cancel does nothing.
+func (r *Reservation) OK() bool {
+	return r.ok
+}
+
+// Delay is shorthand for DelayFrom(time.Now()).
+func (r *Reservation) Delay() time.Duration {
+	return r.DelayFrom(time.Now())
+}
+
+// InfDuration is the duration returned by Delay when a Reservation is not OK.
+const InfDuration = time.Duration(1<<63 - 1)
+
+// DelayFrom returns the duration for which the reservation holder must wait
+// before taking the reserved action.  Zero duration means act immediately.
+// InfDuration means the limiter cannot grant the tokens requested in this
+// Reservation within the maximum wait time.
+func (r *Reservation) DelayFrom(now time.Time) time.Duration {
+	if !r.ok {
+		return InfDuration
+	}
+	delay := r.timeToAct.Sub(now)
+	if delay < 0 {
+		return 0
+	}
+	return delay
+}
+
+// Cancel is shorthand for CancelAt(time.Now()).
+func (r *Reservation) Cancel() {
+	r.CancelAt(time.Now())
+	return
+}
+
+// CancelAt indicates that the reservation holder will not perform the reserved action
+// and reverses the effects of this Reservation on the rate limit as much as possible,
+// considering that other reservations may have already been made.
+func (r *Reservation) CancelAt(now time.Time) {
+	if !r.ok {
+		return
+	}
+
+	r.lim.mu.Lock()
+	defer r.lim.mu.Unlock()
+
+	if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(now) {
+		return
+	}
+
+	// calculate tokens to restore
+	// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
+	// after r was obtained. These tokens should not be restored.
+	restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct))
+	if restoreTokens <= 0 {
+		return
+	}
+	// advance time to now
+	now, _, tokens := r.lim.advance(now)
+	// calculate new number of tokens
+	tokens += restoreTokens
+	if burst := float64(r.lim.burst); tokens > burst {
+		tokens = burst
+	}
+	// update state
+	r.lim.last = now
+	r.lim.tokens = tokens
+	if r.timeToAct == r.lim.lastEvent {
+		prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens)))
+		if !prevEvent.Before(now) {
+			r.lim.lastEvent = prevEvent
+		}
+	}
+
+	return
+}
+
+// Reserve is shorthand for ReserveN(time.Now(), 1).
+func (lim *Limiter) Reserve() *Reservation {
+	return lim.ReserveN(time.Now(), 1)
+}
+
+// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
+// The Limiter takes this Reservation into account when allowing future events.
+// ReserveN returns false if n exceeds the Limiter's burst size.
+// Usage example:
+//   r := lim.ReserveN(time.Now(), 1)
+//   if !r.OK() {
+//     // Not allowed to act! Did you remember to set lim.burst to be > 0 ?
+//     return
+//   }
+//   time.Sleep(r.Delay())
+//   Act()
+// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
+// If you need to respect a deadline or cancel the delay, use Wait instead.
+// To drop or skip events exceeding rate limit, use Allow instead.
+func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation {
+	r := lim.reserveN(now, n, InfDuration)
+	return &r
+}
+
+// Wait is shorthand for WaitN(ctx, 1).
+func (lim *Limiter) Wait(ctx context.Context) (err error) {
+	return lim.WaitN(ctx, 1)
+}
+
+// WaitN blocks until lim permits n events to happen.
+// It returns an error if n exceeds the Limiter's burst size, the Context is
+// canceled, or the expected wait time exceeds the Context's Deadline.
+// The burst limit is ignored if the rate limit is Inf.
+func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
+	if n > lim.burst && lim.limit != Inf {
+		return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, lim.burst)
+	}
+	// Check if ctx is already cancelled
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+	}
+	// Determine wait limit
+	now := time.Now()
+	waitLimit := InfDuration
+	if deadline, ok := ctx.Deadline(); ok {
+		waitLimit = deadline.Sub(now)
+	}
+	// Reserve
+	r := lim.reserveN(now, n, waitLimit)
+	if !r.ok {
+		return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n)
+	}
+	// Wait if necessary
+	delay := r.DelayFrom(now)
+	if delay == 0 {
+		return nil
+	}
+	t := time.NewTimer(delay)
+	defer t.Stop()
+	select {
+	case <-t.C:
+		// We can proceed.
+		return nil
+	case <-ctx.Done():
+		// Context was canceled before we could proceed.  Cancel the
+		// reservation, which may permit other events to proceed sooner.
+		r.Cancel()
+		return ctx.Err()
+	}
+}
+
+// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit).
+func (lim *Limiter) SetLimit(newLimit Limit) {
+	lim.SetLimitAt(time.Now(), newLimit)
+}
+
+// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated
+// or underutilized by those which reserved (using Reserve or Wait) but did not yet act
+// before SetLimitAt was called.
+func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit) {
+	lim.mu.Lock()
+	defer lim.mu.Unlock()
+
+	now, _, tokens := lim.advance(now)
+
+	lim.last = now
+	lim.tokens = tokens
+	lim.limit = newLimit
+}
+
+// reserveN is a helper method for AllowN, ReserveN, and WaitN.
+// maxFutureReserve specifies the maximum reservation wait duration allowed.
+// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN.
+func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
+	lim.mu.Lock()
+
+	if lim.limit == Inf {
+		lim.mu.Unlock()
+		return Reservation{
+			ok:        true,
+			lim:       lim,
+			tokens:    n,
+			timeToAct: now,
+		}
+	}
+
+	now, last, tokens := lim.advance(now)
+
+	// Calculate the remaining number of tokens resulting from the request.
+	tokens -= float64(n)
+
+	// Calculate the wait duration
+	var waitDuration time.Duration
+	if tokens < 0 {
+		waitDuration = lim.limit.durationFromTokens(-tokens)
+	}
+
+	// Decide result
+	ok := n <= lim.burst && waitDuration <= maxFutureReserve
+
+	// Prepare reservation
+	r := Reservation{
+		ok:    ok,
+		lim:   lim,
+		limit: lim.limit,
+	}
+	if ok {
+		r.tokens = n
+		r.timeToAct = now.Add(waitDuration)
+	}
+
+	// Update state
+	if ok {
+		lim.last = now
+		lim.tokens = tokens
+		lim.lastEvent = r.timeToAct
+	} else {
+		lim.last = last
+	}
+
+	lim.mu.Unlock()
+	return r
+}
+
+// advance calculates and returns an updated state for lim resulting from the passage of time.
+// lim is not changed.
+func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
+	last := lim.last
+	if now.Before(last) {
+		last = now
+	}
+
+	// Avoid making delta overflow below when last is very old.
+	maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens)
+	elapsed := now.Sub(last)
+	if elapsed > maxElapsed {
+		elapsed = maxElapsed
+	}
+
+	// Calculate the new number of tokens, due to time that passed.
+	delta := lim.limit.tokensFromDuration(elapsed)
+	tokens := lim.tokens + delta
+	if burst := float64(lim.burst); tokens > burst {
+		tokens = burst
+	}
+
+	return now, last, tokens
+}
+
+// durationFromTokens is a unit conversion function from the number of tokens to the duration
+// of time it takes to accumulate them at a rate of limit tokens per second.
+func (limit Limit) durationFromTokens(tokens float64) time.Duration {
+	seconds := tokens / float64(limit)
+	return time.Nanosecond * time.Duration(1e9*seconds)
+}
+
+// tokensFromDuration is a unit conversion function from a time duration to the number of tokens
+// which could be accumulated during that duration at a rate of limit tokens per second.
+func (limit Limit) tokensFromDuration(d time.Duration) float64 {
+	return d.Seconds() * float64(limit)
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 06b4a2cf4..c74e96202 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -56,6 +56,12 @@
 			"revision": "165db2f241fd235aec29ba6d9b1ccd5f1c14637c",
 			"revisionTime": "2015-01-22T07:26:53Z"
 		},
+		{
+			"checksumSHA1": "WILMZlCPSNbyMzYRNo/RkDcUH2M=",
+			"path": "github.com/cloudflare/cloudflare-go",
+			"revision": "a80f83b9add9d67ca4098ccbf42cd865ebb36ffb",
+			"revisionTime": "2019-09-16T15:18:08Z"
+		},
 		{
 			"checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",
 			"path": "github.com/davecgh/go-spew/spew",
@@ -844,6 +850,12 @@
 			"revision": "31e7599a6c37728c25ca34167be099d072ad335d",
 			"revisionTime": "2019-04-05T05:38:27Z"
 		},
+		{
+			"checksumSHA1": "7Ev/X4Xe8P3961myez/hBKO05ig=",
+			"path": "golang.org/x/time/rate",
+			"revision": "9d24e82272b4f38b78bc8cff74fa936d31ccd8ef",
+			"revisionTime": "2019-02-15T22:48:40Z"
+		},
 		{
 			"checksumSHA1": "CEFTYXtWmgSh+3Ik1NmDaJcz4E0=",
 			"path": "gopkg.in/check.v1",
-- 
GitLab