Repository: xenolf/lego
Pull request #658 (2018-10-02 20:46:44)

cloudflare: use the official go client.
Use the official Cloudfalre go client.

https://github.com/cloudflare/cloudflare-go



Patch:
diff --git a/Gopkg.lock b/Gopkg.lock
index ef50a5b8..428fe61a 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -126,6 +126,14 @@
   revision = "8b15f938ed215522a37275106e847f6f0be85fe8"
   version = "v1.15.23"
 
+[[projects]]
+  digest = "1:03cfacdc6bfd46007c15786c1ece3fa074f89e5193a292f0f26d9e98c99c7cc2"
+  name = "github.com/cloudflare/cloudflare-go"
+  packages = ["."]
+  pruneopts = "NUT"
+  revision = "1f9007fbecae20711133c60519338c41cef1ffb4"
+  version = "v0.8.5"
+
 [[projects]]
   branch = "master"
   digest = "1:2a7013a7e8cfec93dd725f95ea0c4b390d7d71f8a8bc3dbde05cfc9524ba5ea8"
@@ -483,6 +491,14 @@
   revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
   version = "v0.3.0"
 
+[[projects]]
+  branch = "master"
+  digest = "1:c9e7a4b4d47c0ed205d257648b0e5b0440880cb728506e318f8ac7cd36270bc4"
+  name = "golang.org/x/time"
+  packages = ["rate"]
+  pruneopts = "NUT"
+  revision = "fbb02b2291d28baffd63558aa44b4b56f178d650"
+
 [[projects]]
   branch = "master"
   digest = "1:90f61cd4297f7f4ce75f142744673ecdafd9023668cd24730f14033f265e394a"
@@ -574,6 +590,7 @@
     "github.com/aws/aws-sdk-go/aws/session",
     "github.com/aws/aws-sdk-go/service/lightsail",
     "github.com/aws/aws-sdk-go/service/route53",
+    "github.com/cloudflare/cloudflare-go",
     "github.com/cpu/goacmedns",
     "github.com/decker502/dnspod-go",
     "github.com/dnsimple/dnsimple-go/dnsimple",
diff --git a/providers/dns/cloudflare/client.go b/providers/dns/cloudflare/client.go
deleted file mode 100644
index 24c4ae0f..00000000
--- a/providers/dns/cloudflare/client.go
+++ /dev/null
@@ -1,212 +0,0 @@
-package cloudflare
-
-import (
-	"bytes"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net/http"
-
-	"github.com/xenolf/lego/acme"
-)
-
-// defaultBaseURL represents the API endpoint to call.
-const defaultBaseURL = "https://api.cloudflare.com/client/v4"
-
-// APIError contains error details for failed requests
-type APIError struct {
-	Code       int        `json:"code,omitempty"`
-	Message    string     `json:"message,omitempty"`
-	ErrorChain []APIError `json:"error_chain,omitempty"`
-}
-
-// APIResponse represents a response from Cloudflare API
-type APIResponse struct {
-	Success bool            `json:"success"`
-	Errors  []*APIError     `json:"errors"`
-	Result  json.RawMessage `json:"result"`
-}
-
-// TxtRecord represents a Cloudflare DNS record
-type TxtRecord struct {
-	Name    string `json:"name"`
-	Type    string `json:"type"`
-	Content string `json:"content"`
-	ID      string `json:"id,omitempty"`
-	TTL     int    `json:"ttl,omitempty"`
-	ZoneID  string `json:"zone_id,omitempty"`
-}
-
-// HostedZone represents a Cloudflare DNS zone
-type HostedZone struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-}
-
-// Client Cloudflare API client
-type Client struct {
-	authEmail  string
-	authKey    string
-	BaseURL    string
-	HTTPClient *http.Client
-}
-
-// NewClient create a Cloudflare API client
-func NewClient(authEmail string, authKey string) (*Client, error) {
-	if authEmail == "" {
-		return nil, errors.New("cloudflare: some credentials information are missing: email")
-	}
-
-	if authKey == "" {
-		return nil, errors.New("cloudflare: some credentials information are missing: key")
-	}
-
-	return &Client{
-		authEmail:  authEmail,
-		authKey:    authKey,
-		BaseURL:    defaultBaseURL,
-		HTTPClient: http.DefaultClient,
-	}, nil
-}
-
-// GetHostedZoneID get hosted zone
-func (c *Client) GetHostedZoneID(fqdn string) (string, error) {
-	authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
-	if err != nil {
-		return "", err
-	}
-
-	result, err := c.doRequest(http.MethodGet, "/zones?name="+acme.UnFqdn(authZone), nil)
-	if err != nil {
-		return "", err
-	}
-
-	var hostedZone []HostedZone
-	err = json.Unmarshal(result, &hostedZone)
-	if err != nil {
-		return "", fmt.Errorf("cloudflare: HostedZone unmarshaling error: %v", err)
-	}
-
-	count := len(hostedZone)
-	if count == 0 {
-		return "", fmt.Errorf("cloudflare: zone %s not found for domain %s", authZone, fqdn)
-	} else if count > 1 {
-		return "", fmt.Errorf("cloudflare: zone %s cannot be find for domain %s: too many hostedZone: %v", authZone, fqdn, hostedZone)
-	}
-
-	return hostedZone[0].ID, nil
-}
-
-// FindTxtRecord Find a TXT record
-func (c *Client) FindTxtRecord(zoneID, fqdn string) (*TxtRecord, error) {
-	result, err := c.doRequest(
-		http.MethodGet,
-		fmt.Sprintf("/zones/%s/dns_records?per_page=1000&type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	var records []TxtRecord
-	err = json.Unmarshal(result, &records)
-	if err != nil {
-		return nil, fmt.Errorf("cloudflare: record unmarshaling error: %v", err)
-	}
-
-	for _, rec := range records {
-		fmt.Println(rec.Name, acme.UnFqdn(fqdn))
-		if rec.Name == acme.UnFqdn(fqdn) {
-			return &rec, nil
-		}
-	}
-
-	return nil, fmt.Errorf("cloudflare: no existing record found for %s", fqdn)
-}
-
-// AddTxtRecord add a TXT record
-func (c *Client) AddTxtRecord(fqdn string, record TxtRecord) error {
-	zoneID, err := c.GetHostedZoneID(fqdn)
-	if err != nil {
-		return err
-	}
-
-	body, err := json.Marshal(record)
-	if err != nil {
-		return fmt.Errorf("cloudflare: record marshaling error: %v", err)
-	}
-
-	_, err = c.doRequest(http.MethodPost, fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
-	return err
-}
-
-// RemoveTxtRecord Remove a TXT record
-func (c *Client) RemoveTxtRecord(fqdn string) error {
-	zoneID, err := c.GetHostedZoneID(fqdn)
-	if err != nil {
-		return err
-	}
-
-	record, err := c.FindTxtRecord(zoneID, fqdn)
-	if err != nil {
-		return err
-	}
-
-	_, err = c.doRequest(http.MethodDelete, fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
-	return err
-}
-
-func (c *Client) doRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
-	req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.BaseURL, uri), body)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Header.Set("X-Auth-Email", c.authEmail)
-	req.Header.Set("X-Auth-Key", c.authKey)
-
-	resp, err := c.HTTPClient.Do(req)
-	if err != nil {
-		return nil, fmt.Errorf("cloudflare: error querying API: %v", err)
-	}
-
-	defer resp.Body.Close()
-
-	content, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return nil, fmt.Errorf("cloudflare: %s", toUnreadableBodyMessage(req, content))
-	}
-
-	var r APIResponse
-	err = json.Unmarshal(content, &r)
-	if err != nil {
-		return nil, fmt.Errorf("cloudflare: APIResponse unmarshaling error: %v: %s", err, toUnreadableBodyMessage(req, content))
-	}
-
-	if !r.Success {
-		if len(r.Errors) > 0 {
-			return nil, fmt.Errorf("cloudflare: error \n%s", toError(r))
-		}
-
-		return nil, fmt.Errorf("cloudflare: %s", toUnreadableBodyMessage(req, content))
-	}
-
-	return r.Result, nil
-}
-
-func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
-	return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody))
-}
-
-func toError(r APIResponse) error {
-	errStr := ""
-	for _, apiErr := range r.Errors {
-		errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
-		for _, chainErr := range apiErr.ErrorChain {
-			errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
-		}
-	}
-	return fmt.Errorf("cloudflare: error \n%s", errStr)
-}
diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go
index b402bd82..a0d379c0 100644
--- a/providers/dns/cloudflare/cloudflare.go
+++ b/providers/dns/cloudflare/cloudflare.go
@@ -8,12 +8,18 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/cloudflare/cloudflare-go"
 	"github.com/xenolf/lego/acme"
+	"github.com/xenolf/lego/log"
 	"github.com/xenolf/lego/platform/config/env"
 )
 
 // CloudFlareAPIURL represents the API endpoint to call.
-const CloudFlareAPIURL = defaultBaseURL // Deprecated
+const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4" // Deprecated
+
+const (
+	minTTL = 120
+)
 
 // Config is used to configure the creation of the DNSProvider
 type Config struct {
@@ -28,7 +34,7 @@ type Config struct {
 // NewDefaultConfig returns a default configuration for the DNSProvider
 func NewDefaultConfig() *Config {
 	return &Config{
-		TTL:                env.GetOrDefaultInt("CLOUDFLARE_TTL", 120),
+		TTL:                env.GetOrDefaultInt("CLOUDFLARE_TTL", minTTL),
 		PropagationTimeout: env.GetOrDefaultSecond("CLOUDFLARE_PROPAGATION_TIMEOUT", 2*time.Minute),
 		PollingInterval:    env.GetOrDefaultSecond("CLOUDFLARE_POLLING_INTERVAL", 2*time.Second),
 		HTTPClient: &http.Client{
@@ -39,7 +45,7 @@ func NewDefaultConfig() *Config {
 
 // DNSProvider is an implementation of the acme.ChallengeProvider interface
 type DNSProvider struct {
-	client *Client
+	client *cloudflare.API
 	config *Config
 }
 
@@ -78,20 +84,19 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 		return nil, errors.New("cloudflare: the configuration of the DNS provider is nil")
 	}
 
-	client, err := NewClient(config.AuthEmail, config.AuthKey)
+	if config.TTL < minTTL {
+		config.TTL = minTTL
+	}
+
+	client, err := cloudflare.New(config.AuthKey, config.AuthEmail, cloudflare.HTTPClient(config.HTTPClient))
 	if err != nil {
 		return nil, err
 	}
 
-	client.HTTPClient = config.HTTPClient
-
 	// TODO: must be remove. keep only for compatibility reason.
 	client.BaseURL = CloudFlareAPIURL
 
-	return &DNSProvider{
-		client: client,
-		config: config,
-	}, nil
+	return &DNSProvider{client: client, config: config}, nil
 }
 
 // Timeout returns the timeout and interval to use when checking for DNS propagation.
@@ -104,19 +109,67 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 	fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
 
-	rec := TxtRecord{
+	authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+	if err != nil {
+		return fmt.Errorf("cloudflare: %v", err)
+	}
+
+	zoneID, err := d.client.ZoneIDByName(authZone)
+	if err != nil {
+		return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err)
+	}
+
+	dnsRecord := cloudflare.DNSRecord{
 		Type:    "TXT",
 		Name:    acme.UnFqdn(fqdn),
 		Content: value,
 		TTL:     d.config.TTL,
 	}
 
-	return d.client.AddTxtRecord(fqdn, rec)
+	response, _ := d.client.CreateDNSRecord(zoneID, dnsRecord)
+	if err != nil {
+		return fmt.Errorf("cloudflare: failed to create TXT record: %v", err)
+	}
+
+	if !response.Success {
+		return fmt.Errorf("cloudflare: failed to create TXT record: %+v %+v", response.Errors, response.Messages)
+	}
+
+	log.Infof("cloudflare: new record for %s, ID %s", domain, response.Result.ID)
+
+	return nil
 }
 
 // CleanUp removes the TXT record matching the specified parameters
 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 	fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
 
-	return d.client.RemoveTxtRecord(fqdn)
+	authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers)
+	if err != nil {
+		return fmt.Errorf("cloudflare: %v", err)
+	}
+
+	zoneID, err := d.client.ZoneIDByName(authZone)
+	if err != nil {
+		return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err)
+	}
+
+	dnsRecord := cloudflare.DNSRecord{
+		Type: "TXT",
+		Name: acme.UnFqdn(fqdn),
+	}
+
+	records, err := d.client.DNSRecords(zoneID, dnsRecord)
+	if err != nil {
+		return fmt.Errorf("cloudflare: failed to find TXT records: %v", err)
+	}
+
+	for _, record := range records {
+		err = d.client.DeleteDNSRecord(zoneID, record.ID)
+		if err != nil {
+			log.Printf("cloudflare: failed to delete TXT record: %v", err)
+		}
+	}
+
+	return nil
 }
diff --git a/vendor/github.com/cloudflare/cloudflare-go/LICENSE b/vendor/github.com/cloudflare/cloudflare-go/LICENSE
new file mode 100644
index 00000000..c035bb48
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2015-2016, 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/cloudflare.go b/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go
new file mode 100644
index 00000000..818cdf8a
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/cloudflare.go
@@ -0,0 +1,318 @@
+// Package cloudflare implements the Cloudflare v4 API.
+package cloudflare
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"log"
+	"math"
+	"net/http"
+	"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
+)
+
+// 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
+	BaseURL           string
+	organizationID    string
+	headers           http.Header
+	httpClient        *http.Client
+	authType          int
+	rateLimiter       *rate.Limiter
+	retryPolicy       RetryPolicy
+	logger            Logger
+}
+
+// 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)
+	}
+
+	silentLogger := log.New(ioutil.Discard, "", log.LstdFlags)
+
+	api := &API{
+		APIKey:      key,
+		APIEmail:    email,
+		BaseURL:     apiURL,
+		headers:     make(http.Header),
+		authType:    AuthKeyEmail,
+		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
+}
+
+// SetAuthType sets the authentication method (AuthyKeyEmail 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.ListZones(zoneName)
+	if err != nil {
+		return "", errors.Wrap(err, "ListZones command failed")
+	}
+	for _, zone := range res {
+		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 returnng. params will be serialized to JSON.
+func (api *API) makeRequest(method, uri string, params interface{}) ([]byte, error) {
+	return api.makeRequestWithAuthType(method, uri, params, api.authType)
+}
+
+func (api *API) makeRequestWithAuthType(method, uri string, params interface{}, authType int) ([]byte, error) {
+	// Replace nil with a JSON object if needed
+	var jsonBody []byte
+	var err error
+	if params != nil {
+		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
+			// dont 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)
+		}
+		api.rateLimiter.Wait(context.TODO())
+		if err != nil {
+			return nil, errors.Wrap(err, "Error caused by request rate limiting")
+		}
+		resp, respErr = api.request(method, uri, reqBody, authType)
+
+		// 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)
+	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(method, uri string, reqBody io.Reader, authType int) (*http.Response, error) {
+	req, err := http.NewRequest(method, api.BaseURL+uri, reqBody)
+	if err != nil {
+		return nil, errors.Wrap(err, "HTTP request creation failed")
+	}
+
+	// Apply any user-defined headers first.
+	req.Header = cloneHeader(api.headers)
+	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 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 both accounts and organizations.
+// If an Organization option was used when creating the API instance, returns the org 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.organizationID != "" {
+		return "/organizations/" + api.organizationID
+	}
+	return accountBase
+}
+
+// cloneHeader returns a shallow copy of the header.
+// copied from https://godoc.org/github.com/golang/gddo/httputil/header#Copy
+func cloneHeader(header http.Header) http.Header {
+	h := make(http.Header)
+	for k, vs := range header {
+		h[k] = vs
+	}
+	return h
+}
+
+// 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{})
+}
diff --git a/vendor/github.com/cloudflare/cloudflare-go/cpage.go b/vendor/github.com/cloudflare/cloudflare-go/cpage.go
new file mode 100644
index 00000000..87e50ce8
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/cpage.go
@@ -0,0 +1,29 @@
+package cloudflare
+
+import "time"
+
+// CustomPage represents a custom page configuration.
+type CustomPage struct {
+	CreatedOn      string    `json:"created_on"`
+	ModifiedOn     time.Time `json:"modified_on"`
+	URL            string    `json:"url"`
+	State          string    `json:"state"`
+	RequiredTokens []string  `json:"required_tokens"`
+	PreviewTarget  string    `json:"preview_target"`
+	Description    string    `json:"description"`
+}
+
+// CustomPageResponse represents the response from the custom pages endpoint.
+type CustomPageResponse struct {
+	Response
+	Result []CustomPage `json:"result"`
+}
+
+// https://api.cloudflare.com/#custom-pages-for-a-zone-available-custom-pages
+// GET /zones/:zone_identifier/custom_pages
+
+// https://api.cloudflare.com/#custom-pages-for-a-zone-custom-page-details
+// GET /zones/:zone_identifier/custom_pages/:identifier
+
+// https://api.cloudflare.com/#custom-pages-for-a-zone-update-custom-page-url
+// PUT /zones/:zone_identifier/custom_pages/:identifier
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 00000000..44dbc406
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/custom_hostname.go
@@ -0,0 +1,149 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+
+	"github.com/pkg/errors"
+)
+
+// 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_name,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"`
+	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"`
+}
+
+// Modify 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")
+}
+
+// Delete 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/dns.go b/vendor/github.com/cloudflare/cloudflare-go/dns.go
new file mode 100644
index 00000000..3303e9d0
--- /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,omitempty"`
+	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,omitempty"`
+}
+
+// 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("PUT", 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/errors.go b/vendor/github.com/cloudflare/cloudflare-go/errors.go
new file mode 100644
index 00000000..2fff3db3
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/errors.go
@@ -0,0 +1,48 @@
+package cloudflare
+
+// Error messages
+const (
+	errEmptyCredentials     = "invalid credentials: key & email must not be empty"
+	errMakeRequestError     = "error from makeRequest"
+	errUnmarshalError       = "error unmarshalling the JSON response"
+	errRequestNotSuccessful = "error reported by API"
+)
+
+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/firewall.go b/vendor/github.com/cloudflare/cloudflare-go/firewall.go
new file mode 100644
index 00000000..9246b7e6
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/firewall.go
@@ -0,0 +1,241 @@
+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)
+}
+
+// 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)
+}
+
+// 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)
+}
+
+// ListOrganizationAccessRules returns a slice of access rules for the given
+// organization identifier.
+//
+// This takes an AccessRule to allow filtering of the results returned.
+//
+// API reference: https://api.cloudflare.com/#organization-level-firewall-access-rule-list-access-rules
+func (api *API) ListOrganizationAccessRules(organizationID string, accessRule AccessRule, page int) (*AccessRuleListResponse, error) {
+	return api.listAccessRules("/organizations/"+organizationID, accessRule, page)
+}
+
+// CreateOrganizationAccessRule creates a firewall access rule for the given
+// organization identifier.
+//
+// API reference: https://api.cloudflare.com/#organization-level-firewall-access-rule-create-access-rule
+func (api *API) CreateOrganizationAccessRule(organizationID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.createAccessRule("/organizations/"+organizationID, accessRule)
+}
+
+// UpdateOrganizationAccessRule updates a single access rule for the given
+// organization & access rule identifiers.
+//
+// API reference: https://api.cloudflare.com/#organization-level-firewall-access-rule-update-access-rule
+func (api *API) UpdateOrganizationAccessRule(organizationID, accessRuleID string, accessRule AccessRule) (*AccessRuleResponse, error) {
+	return api.updateAccessRule("/organizations/"+organizationID, accessRuleID, accessRule)
+}
+
+// DeleteOrganizationAccessRule deletes a single access rule for the given
+// organization and access rule identifiers.
+//
+// API reference: https://api.cloudflare.com/#organization-level-firewall-access-rule-delete-access-rule
+func (api *API) DeleteOrganizationAccessRule(organizationID, accessRuleID string) (*AccessRuleResponse, error) {
+	return api.deleteAccessRule("/organizations/"+organizationID, 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) 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/ips.go b/vendor/github.com/cloudflare/cloudflare-go/ips.go
new file mode 100644
index 00000000..72b5fcfb
--- /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 00000000..c5cc8391
--- /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 00000000..e1af07b5
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/load_balancing.go
@@ -0,0 +1,330 @@
+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"`
+}
+
+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"`
+	ExpectedBody  string              `json:"expected_body"`
+	ExpectedCodes string              `json:"expected_codes"`
+}
+
+// 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"`
+	Persistence  string              `json:"session_affinity,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"`
+}
+
+// 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
+}
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 00000000..3dba81c2
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/lockdown.go
@@ -0,0 +1,150 @@
+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"`
+}
+
+// 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"
+	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/options.go b/vendor/github.com/cloudflare/cloudflare-go/options.go
new file mode 100644
index 00000000..1a113ee1
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/options.go
@@ -0,0 +1,90 @@
+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
+	}
+}
+
+// Organization allows you to apply account-level changes (Load Balancing, Railguns)
+// to an organization instead.
+func UsingOrganization(orgID string) Option {
+	return func(api *API) error {
+		api.organizationID = orgID
+		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
+	}
+}
+
+// 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/organizations.go b/vendor/github.com/cloudflare/cloudflare-go/organizations.go
new file mode 100644
index 00000000..97a67e77
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/organizations.go
@@ -0,0 +1,185 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// Organization represents a multi-user organization.
+type Organization struct {
+	ID          string   `json:"id,omitempty"`
+	Name        string   `json:"name,omitempty"`
+	Status      string   `json:"status,omitempty"`
+	Permissions []string `json:"permissions,omitempty"`
+	Roles       []string `json:"roles,omitempty"`
+}
+
+// organizationResponse represents the response from the Organization endpoint.
+type organizationResponse struct {
+	Response
+	Result     []Organization `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// OrganizationMember has details on a member.
+type OrganizationMember struct {
+	ID     string             `json:"id,omitempty"`
+	Name   string             `json:"name,omitempty"`
+	Email  string             `json:"email,omitempty"`
+	Status string             `json:"status,omitempty"`
+	Roles  []OrganizationRole `json:"roles,omitempty"`
+}
+
+// OrganizationInvite has details on an invite.
+type OrganizationInvite struct {
+	ID                 string             `json:"id,omitempty"`
+	InvitedMemberID    string             `json:"invited_member_id,omitempty"`
+	InvitedMemberEmail string             `json:"invited_member_email,omitempty"`
+	OrganizationID     string             `json:"organization_id,omitempty"`
+	OrganizationName   string             `json:"organization_name,omitempty"`
+	Roles              []OrganizationRole `json:"roles,omitempty"`
+	InvitedBy          string             `json:"invited_by,omitempty"`
+	InvitedOn          *time.Time         `json:"invited_on,omitempty"`
+	ExpiresOn          *time.Time         `json:"expires_on,omitempty"`
+	Status             string             `json:"status,omitempty"`
+}
+
+// OrganizationRole has details on a role.
+type OrganizationRole struct {
+	ID          string   `json:"id,omitempty"`
+	Name        string   `json:"name,omitempty"`
+	Description string   `json:"description,omitempty"`
+	Permissions []string `json:"permissions,omitempty"`
+}
+
+// OrganizationDetails represents details of an organization.
+type OrganizationDetails struct {
+	ID      string               `json:"id,omitempty"`
+	Name    string               `json:"name,omitempty"`
+	Members []OrganizationMember `json:"members"`
+	Invites []OrganizationInvite `json:"invites"`
+	Roles   []OrganizationRole   `json:"roles,omitempty"`
+}
+
+// organizationDetailsResponse represents the response from the OrganizationDetails endpoint.
+type organizationDetailsResponse struct {
+	Response
+	Result OrganizationDetails `json:"result"`
+}
+
+// ListOrganizations lists organizations of the logged-in user.
+//
+// API reference: https://api.cloudflare.com/#user-s-organizations-list-organizations
+func (api *API) ListOrganizations() ([]Organization, ResultInfo, error) {
+	var r organizationResponse
+	res, err := api.makeRequest("GET", "/user/organizations", nil)
+	if err != nil {
+		return []Organization{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []Organization{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, r.ResultInfo, nil
+}
+
+// OrganizationDetails returns details for the specified organization of the logged-in user.
+//
+// API reference: https://api.cloudflare.com/#organizations-organization-details
+func (api *API) OrganizationDetails(organizationID string) (OrganizationDetails, error) {
+	var r organizationDetailsResponse
+	uri := "/organizations/" + organizationID
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return OrganizationDetails{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return OrganizationDetails{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, nil
+}
+
+// organizationMembersResponse represents the response from the Organization members endpoint.
+type organizationMembersResponse struct {
+	Response
+	Result     []OrganizationMember `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// OrganizationMembers returns list of members for specified organization of the logged-in user.
+//
+// API reference: https://api.cloudflare.com/#organization-members-list-members
+func (api *API) OrganizationMembers(organizationID string) ([]OrganizationMember, ResultInfo, error) {
+	var r organizationMembersResponse
+	uri := "/organizations/" + organizationID + "/members"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []OrganizationMember{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []OrganizationMember{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, r.ResultInfo, nil
+}
+
+// organizationInvitesResponse represents the response from the Organization invites endpoint.
+type organizationInvitesResponse struct {
+	Response
+	Result     []OrganizationInvite `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// OrganizationMembers returns list of invites for specified organization of the logged-in user.
+//
+// API reference: https://api.cloudflare.com/#organization-invites
+func (api *API) OrganizationInvites(organizationID string) ([]OrganizationInvite, ResultInfo, error) {
+	var r organizationInvitesResponse
+	uri := "/organizations/" + organizationID + "/invites"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []OrganizationInvite{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []OrganizationInvite{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, r.ResultInfo, nil
+}
+
+// organizationRolesResponse represents the response from the Organization roles endpoint.
+type organizationRolesResponse struct {
+	Response
+	Result     []OrganizationRole `json:"result"`
+	ResultInfo `json:"result_info"`
+}
+
+// OrganizationRoles returns list of roles for specified organization of the logged-in user.
+//
+// API reference: https://api.cloudflare.com/#organization-roles-list-roles
+func (api *API) OrganizationRoles(organizationID string) ([]OrganizationRole, ResultInfo, error) {
+	var r organizationRolesResponse
+	uri := "/organizations/" + organizationID + "/roles"
+	res, err := api.makeRequest("GET", uri, nil)
+	if err != nil {
+		return []OrganizationRole{}, ResultInfo{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return []OrganizationRole{}, ResultInfo{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return r.Result, r.ResultInfo, 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 00000000..486033af
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/origin_ca.go
@@ -0,0 +1,168 @@
+package cloudflare
+
+import (
+	"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("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("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("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("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 00000000..8749b2c5
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/page_rules.go
@@ -0,0 +1,206 @@
+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
+  browser_cache_ttl
+  browser_check
+  cache_level
+  disable_apps
+  disable_performance
+  disable_railgun
+  disable_security
+  edge_cache_ttl
+  email_obfuscation
+  forwarding_url
+  ip_geolocation
+  mirage
+  rocket_loader
+  security_level
+  server_side_exclude
+  smart_errors
+  ssl
+  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{}
+	"browser_cache_ttl":   "Browser Cache TTL",        // Value of type int
+	"browser_check":       "Browser Integrity Check",  // Value of type string
+	"cache_level":         "Cache Level",              // 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
+	"forwarding_url":      "Forwarding URL",           // Value of type map[string]interface
+	"ip_geolocation":      "IP Geolocation Header",    // Value of type string
+	"mirage":              "Mirage",                   // 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
+	"smart_errors":        "Smart Errors",             // Value of type string
+	"ssl":                 "SSL",                      // 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, 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
+}
+
+// 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 00000000..72d22869
--- /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 00000000..96ad70ee
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/rate_limiting.go
@@ -0,0 +1,195 @@
+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"`
+}
+
+// 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
+}
+
+// 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"`
+}
+
+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/ssl.go b/vendor/github.com/cloudflare/cloudflare-go/ssl.go
new file mode 100644
index 00000000..def81ea0
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/ssl.go
@@ -0,0 +1,148 @@
+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"`
+	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"`
+}
+
+// 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"`
+}
+
+// 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/user.go b/vendor/github.com/cloudflare/cloudflare-go/user.go
new file mode 100644
index 00000000..b42641e9
--- /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"`
+	Organizations []Organization `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 00000000..6d75f3a1
--- /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 00000000..6eb7629f
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/virtualdns.go
@@ -0,0 +1,125 @@
+package cloudflare
+
+import (
+	"encoding/json"
+
+	"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"`
+}
+
+// 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"`
+}
+
+// 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
+}
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 00000000..4ed15623
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/waf.go
@@ -0,0 +1,97 @@
+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"`
+}
+
+// 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 rule endpoint.
+type WAFRulesResponse struct {
+	Response
+	Result     []WAFRule  `json:"result"`
+	ResultInfo ResultInfo `json:"result_info"`
+}
+
+// ListWAFPackages returns a slice of the WAF packages for the given zone.
+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
+}
+
+// ListWAFRules returns a slice of the WAF rules for the given WAF package.
+func (api *API) ListWAFRules(zoneID, packageID string) ([]WAFRule, error) {
+	var r WAFRulesResponse
+	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)
+	}
+	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
+}
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 00000000..fe222d33
--- /dev/null
+++ b/vendor/github.com/cloudflare/cloudflare-go/zone.go
@@ -0,0 +1,587 @@
+package cloudflare
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// Owner describes the resource owner.
+type Owner struct {
+	ID        string `json:"id"`
+	Email     string `json:"email"`
+	OwnerType string `json:"owner_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              ZoneRatePlan `json:"plan"`
+	PlanPending       ZoneRatePlan `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"`
+}
+
+// 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"`
+}
+
+// ZoneRatePlan contains the plan information for a zone.
+type ZoneRatePlan struct {
+	ID         string                   `json:"id"`
+	Name       string                   `json:"name,omitempty"`
+	Price      int                      `json:"price,omitempty"`
+	Currency   string                   `json:"currency,omitempty"`
+	Duration   int                      `json:"duration,omitempty"`
+	Frequency  string                   `json:"frequency,omitempty"`
+	Components []zoneRatePlanComponents `json:"components,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"`
+}
+
+// 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
+}
+
+// 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"`
+}
+
+// ZoneSettingResponse 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"`
+	// We use a pointer to get a nil type when the field is empty.
+	// This allows us to completely omit this with json.Marshal().
+	Organization *Organization `json:"organization,omitempty"`
+}
+
+// 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 Organization is non-empty, it must have at least the ID field populated.
+// This will add the new zone to the specified multi-user organization.
+//
+// API reference: https://api.cloudflare.com/#zone-create-a-zone
+func (api *API) CreateZone(name string, jumpstart bool, org Organization) (Zone, error) {
+	var newzone newZone
+	newzone.Name = name
+	newzone.JumpStart = jumpstart
+	if org.ID != "" {
+		newzone.Organization = &org
+	}
+
+	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 {
+		// TODO: Paginate here. We only grab the first page of results.
+		// Could do this concurrently after the first request by creating a
+		// sync.WaitGroup or just a channel + workers.
+		res, err = api.makeRequest("GET", "/zones", nil)
+		if err != nil {
+			return []Zone{}, errors.Wrap(err, errMakeRequestError)
+		}
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return []Zone{}, errors.Wrap(err, errUnmarshalError)
+		}
+		zones = r.Result
+	}
+
+	return zones, 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     *ZoneRatePlan `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
+}
+
+// ZoneSetRatePlan changes the zone plan.
+func (api *API) ZoneSetRatePlan(zoneID string, plan ZoneRatePlan) (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("DELETE", 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("DELETE", 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
+}
+
+// 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
+}
diff --git a/vendor/golang.org/x/time/AUTHORS b/vendor/golang.org/x/time/AUTHORS
new file mode 100644
index 00000000..15167cd7
--- /dev/null
+++ b/vendor/golang.org/x/time/AUTHORS
@@ -0,0 +1,3 @@
+# This source code refers to The Go Authors for copyright purposes.
+# The master list of authors is in the main Go distribution,
+# visible at http://tip.golang.org/AUTHORS.
diff --git a/vendor/golang.org/x/time/CONTRIBUTORS b/vendor/golang.org/x/time/CONTRIBUTORS
new file mode 100644
index 00000000..1c4577e9
--- /dev/null
+++ b/vendor/golang.org/x/time/CONTRIBUTORS
@@ -0,0 +1,3 @@
+# This source code was written by the Go contributors.
+# The master list of contributors is in the main Go distribution,
+# visible at http://tip.golang.org/CONTRIBUTORS.
diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE
new file mode 100644
index 00000000..6a66aea5
--- /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 00000000..73309904
--- /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 00000000..7228d97e
--- /dev/null
+++ b/vendor/golang.org/x/time/rate/rate.go
@@ -0,0 +1,384 @@
+// 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 (
+	"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
+}
+
+// contextContext is a temporary(?) copy of the context.Context type
+// to support both Go 1.6 using golang.org/x/net/context and Go 1.7+
+// with the built-in context package. If people ever stop using Go 1.6
+// we can remove this.
+type contextContext interface {
+	Deadline() (deadline time.Time, ok bool)
+	Done() <-chan struct{}
+	Err() error
+	Value(key interface{}) interface{}
+}
+
+// Wait is shorthand for WaitN(ctx, 1).
+func (lim *Limiter) wait(ctx contextContext) (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 contextContext, 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/golang.org/x/time/rate/rate_go16.go b/vendor/golang.org/x/time/rate/rate_go16.go
new file mode 100644
index 00000000..6bab1850
--- /dev/null
+++ b/vendor/golang.org/x/time/rate/rate_go16.go
@@ -0,0 +1,21 @@
+// Copyright 2017 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.
+
+// +build !go1.7
+
+package rate
+
+import "golang.org/x/net/context"
+
+// 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.
+func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
+	return lim.waitN(ctx, n)
+}
diff --git a/vendor/golang.org/x/time/rate/rate_go17.go b/vendor/golang.org/x/time/rate/rate_go17.go
new file mode 100644
index 00000000..f90d85f5
--- /dev/null
+++ b/vendor/golang.org/x/time/rate/rate_go17.go
@@ -0,0 +1,21 @@
+// Copyright 2017 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.
+
+// +build go1.7
+
+package rate
+
+import "context"
+
+// 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.
+func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) {
+	return lim.waitN(ctx, n)
+}


Test patch:
diff --git a/acme/crypto_test.go b/acme/crypto_test.go
index 242b2cb6..c57cdc26 100644
--- a/acme/crypto_test.go
+++ b/acme/crypto_test.go
@@ -53,7 +53,7 @@ func TestPEMCertExpiration(t *testing.T) {
 
 	// Some random string should return an error.
 	ctime, err := GetPEMCertExpiration(buf.Bytes())
-	assert.Errorf(t, err, "Expected getCertExpiration to return an error for garbage string but returned %v", ctime)
+	require.Errorf(t, err, "Expected getCertExpiration to return an error for garbage string but returned %v", ctime)
 
 	// A DER encoded certificate should return an error.
 	_, err = GetPEMCertExpiration(certBytes)
diff --git a/providers/dns/cloudflare/client_test.go b/providers/dns/cloudflare/client_test.go
deleted file mode 100644
index 46fb4cfa..00000000
--- a/providers/dns/cloudflare/client_test.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package cloudflare
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func handlerMock(method string, response *APIResponse, data interface{}) http.Handler {
-	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
-		if req.Method != method {
-			content, err := json.Marshal(APIResponse{
-				Success: false,
-				Errors: []*APIError{
-					{
-						Code:       666,
-						Message:    fmt.Sprintf("invalid method: got %s want %s", req.Method, method),
-						ErrorChain: nil,
-					},
-				},
-			})
-			if err != nil {
-				http.Error(rw, err.Error(), http.StatusInternalServerError)
-				return
-			}
-
-			http.Error(rw, string(content), http.StatusBadRequest)
-			return
-		}
-
-		jsonData, err := json.Marshal(data)
-		if err != nil {
-			http.Error(rw, err.Error(), http.StatusInternalServerError)
-			return
-		}
-
-		response.Result = jsonData
-
-		content, err := json.Marshal(response)
-		if err != nil {
-			http.Error(rw, err.Error(), http.StatusInternalServerError)
-			return
-		}
-
-		_, err = rw.Write(content)
-		if err != nil {
-			http.Error(rw, err.Error(), http.StatusInternalServerError)
-			return
-		}
-	})
-}
-
-func TestClient_GetHostedZoneID(t *testing.T) {
-	type result struct {
-		zoneID string
-		error  bool
-	}
-
-	testCases := []struct {
-		desc     string
-		fqdn     string
-		response *APIResponse
-		data     []HostedZone
-		expected result
-	}{
-		{
-			desc:     "zone found",
-			fqdn:     "_acme-challenge.foo.com.",
-			response: &APIResponse{Success: true},
-			data: []HostedZone{
-				{
-					ID:   "A",
-					Name: "ZONE_A",
-				},
-			},
-			expected: result{zoneID: "A"},
-		},
-		{
-			desc:     "no many zones",
-			fqdn:     "_acme-challenge.foo.com.",
-			response: &APIResponse{Success: true},
-			data: []HostedZone{
-				{
-					ID:   "A",
-					Name: "ZONE_A",
-				},
-				{
-					ID:   "B",
-					Name: "ZONE_B",
-				},
-			},
-			expected: result{error: true},
-		},
-		{
-			desc:     "no zone found",
-			fqdn:     "_acme-challenge.foo.com.",
-			response: &APIResponse{Success: true},
-			expected: result{error: true},
-		},
-	}
-
-	for _, test := range testCases {
-		t.Run(test.desc, func(t *testing.T) {
-			server := httptest.NewServer(handlerMock(http.MethodGet, test.response, test.data))
-
-			client, _ := NewClient("authEmail", "authKey")
-			client.BaseURL = server.URL
-
-			zoneID, err := client.GetHostedZoneID(test.fqdn)
-
-			if test.expected.error {
-				require.Error(t, err)
-			} else {
-				require.NoError(t, err)
-				assert.Equal(t, test.expected.zoneID, zoneID)
-			}
-		})
-	}
-}
-
-func TestClient_FindTxtRecord(t *testing.T) {
-	type result struct {
-		txtRecord *TxtRecord
-		error     bool
-	}
-
-	testCases := []struct {
-		desc     string
-		fqdn     string
-		zoneID   string
-		response *APIResponse
-		data     []TxtRecord
-		expected result
-	}{
-		{
-			desc:     "TXT record found",
-			fqdn:     "_acme-challenge.foo.com.",
-			zoneID:   "ZONE_A",
-			response: &APIResponse{Success: true},
-			data: []TxtRecord{
-				{
-					Name:    "_acme-challenge.foo.com",
-					Type:    "TXT",
-					Content: "txtTXTtxtTXTtxtTXTtxtTXT",
-					ID:      "A",
-					TTL:     50,
-					ZoneID:  "ZONE_A",
-				},
-			},
-			expected: result{
-				txtRecord: &TxtRecord{
-					Name:    "_acme-challenge.foo.com",
-					Type:    "TXT",
-					Content: "txtTXTtxtTXTtxtTXTtxtTXT",
-					ID:      "A",
-					TTL:     50,
-					ZoneID:  "ZONE_A",
-				},
-			},
-		},
-		{
-			desc:     "TXT record not found",
-			fqdn:     "_acme-challenge.foo.com.",
-			zoneID:   "ZONE_A",
-			response: &APIResponse{Success: true},
-			expected: result{error: true},
-		},
-	}
-
-	for _, test := range testCases {
-		t.Run(test.desc, func(t *testing.T) {
-			server := httptest.NewServer(handlerMock(http.MethodGet, test.response, test.data))
-
-			client, _ := NewClient("authEmail", "authKey")
-			client.BaseURL = server.URL
-
-			txtRecord, err := client.FindTxtRecord(test.zoneID, test.fqdn)
-
-			if test.expected.error {
-				require.Error(t, err)
-			} else {
-				require.NoError(t, err)
-				assert.Equal(t, test.expected.txtRecord, txtRecord)
-			}
-		})
-	}
-}
diff --git a/providers/dns/cloudflare/cloudflare_test.go b/providers/dns/cloudflare/cloudflare_test.go
index 7fd10704..9214f32c 100644
--- a/providers/dns/cloudflare/cloudflare_test.go
+++ b/providers/dns/cloudflare/cloudflare_test.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 var (
@@ -29,44 +30,116 @@ func restoreEnv() {
 	os.Setenv("CLOUDFLARE_API_KEY", cflareAPIKey)
 }
 
-func TestNewDNSProviderValid(t *testing.T) {
-	os.Setenv("CLOUDFLARE_EMAIL", "")
-	os.Setenv("CLOUDFLARE_API_KEY", "")
-	defer restoreEnv()
-
-	config := NewDefaultConfig()
-	config.AuthEmail = "123"
-	config.AuthKey = "123"
-
-	_, err := NewDNSProviderConfig(config)
-
-	assert.NoError(t, err)
-}
-
-func TestNewDNSProviderValidEnv(t *testing.T) {
-	defer restoreEnv()
-	os.Setenv("CLOUDFLARE_EMAIL", "test@example.com")
-	os.Setenv("CLOUDFLARE_API_KEY", "123")
-
-	_, err := NewDNSProvider()
-	assert.NoError(t, err)
-}
-
-func TestNewDNSProviderMissingCredErr(t *testing.T) {
-	defer restoreEnv()
-	os.Setenv("CLOUDFLARE_EMAIL", "")
-	os.Setenv("CLOUDFLARE_API_KEY", "")
+func TestNewDNSProvider(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		envVars  map[string]string
+		expected string
+	}{
+		{
+			desc: "success",
+			envVars: map[string]string{
+				"CLOUDFLARE_EMAIL":   "test@example.com",
+				"CLOUDFLARE_API_KEY": "123",
+			},
+		},
+		{
+			desc: "missing credentials",
+			envVars: map[string]string{
+				"CLOUDFLARE_EMAIL":   "",
+				"CLOUDFLARE_API_KEY": "",
+			},
+			expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY",
+		},
+		{
+			desc: "missing email",
+			envVars: map[string]string{
+				"CLOUDFLARE_EMAIL":   "",
+				"CLOUDFLARE_API_KEY": "key",
+			},
+			expected: "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL",
+		},
+		{
+			desc: "missing api key",
+			envVars: map[string]string{
+				"CLOUDFLARE_EMAIL":   "awesome@possum.com",
+				"CLOUDFLARE_API_KEY": "",
+			},
+			expected: "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY",
+		},
+	}
 
-	_, err := NewDNSProvider()
-	assert.EqualError(t, err, "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY")
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			defer restoreEnv()
+			for key, value := range test.envVars {
+				if len(value) == 0 {
+					os.Unsetenv(key)
+				} else {
+					os.Setenv(key, value)
+				}
+			}
+
+			p, err := NewDNSProvider()
+
+			if len(test.expected) == 0 {
+				assert.NoError(t, err)
+				assert.NotNil(t, p)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
 }
 
-func TestNewDNSProviderMissingCredErrSingle(t *testing.T) {
-	defer restoreEnv()
-	os.Setenv("CLOUDFLARE_EMAIL", "awesome@possum.com")
+func TestNewDNSProviderConfig(t *testing.T) {
+	testCases := []struct {
+		desc      string
+		authEmail string
+		authKey   string
+		expected  string
+	}{
+		{
+			desc:      "success",
+			authEmail: "test@example.com",
+			authKey:   "123",
+		},
+		{
+			desc:     "missing credentials",
+			expected: "invalid credentials: key & email must not be empty",
+		},
+		{
+			desc:     "missing email",
+			authKey:  "123",
+			expected: "invalid credentials: key & email must not be empty",
+		},
+		{
+			desc:      "missing api key",
+			authEmail: "test@example.com",
+			expected:  "invalid credentials: key & email must not be empty",
+		},
+	}
 
-	_, err := NewDNSProvider()
-	assert.EqualError(t, err, "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY")
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			defer restoreEnv()
+			os.Unsetenv("CLOUDFLARE_EMAIL")
+			os.Unsetenv("CLOUDFLARE_API_KEY")
+
+			config := NewDefaultConfig()
+			config.AuthEmail = test.authEmail
+			config.AuthKey = test.authKey
+
+			p, err := NewDNSProviderConfig(config)
+
+			if len(test.expected) == 0 {
+				assert.NoError(t, err)
+				assert.NotNil(t, p)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
 }
 
 func TestCloudFlarePresent(t *testing.T) {
@@ -79,10 +152,10 @@ func TestCloudFlarePresent(t *testing.T) {
 	config.AuthKey = cflareAPIKey
 
 	provider, err := NewDNSProviderConfig(config)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	err = provider.Present(cflareDomain, "", "123d==")
-	assert.NoError(t, err)
+	require.NoError(t, err)
 }
 
 func TestCloudFlareCleanUp(t *testing.T) {
@@ -97,8 +170,8 @@ func TestCloudFlareCleanUp(t *testing.T) {
 	config.AuthKey = cflareAPIKey
 
 	provider, err := NewDNSProviderConfig(config)
-	assert.NoError(t, err)
+	require.NoError(t, err)
 
 	err = provider.CleanUp(cflareDomain, "", "123d==")
-	assert.NoError(t, err)
+	require.NoError(t, err)
 }