From 18fe57183defff61035690f895e1268fc0ede2b8 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 3 Oct 2018 00:02:01 +0200 Subject: [PATCH] cloudflare: use the official go client. (#658) --- Gopkg.lock | 17 + acme/crypto_test.go | 2 +- providers/dns/cloudflare/client.go | 212 ------- providers/dns/cloudflare/client_test.go | 192 ------ providers/dns/cloudflare/cloudflare.go | 79 ++- providers/dns/cloudflare/cloudflare_test.go | 139 ++++- .../cloudflare/cloudflare-go/LICENSE | 26 + .../cloudflare/cloudflare-go/cloudflare.go | 318 ++++++++++ .../cloudflare/cloudflare-go/cpage.go | 29 + .../cloudflare-go/custom_hostname.go | 149 +++++ .../cloudflare/cloudflare-go/dns.go | 174 ++++++ .../cloudflare/cloudflare-go/errors.go | 48 ++ .../cloudflare/cloudflare-go/firewall.go | 241 +++++++ .../cloudflare/cloudflare-go/ips.go | 44 ++ .../cloudflare/cloudflare-go/keyless.go | 52 ++ .../cloudflare-go/load_balancing.go | 330 ++++++++++ .../cloudflare/cloudflare-go/lockdown.go | 150 +++++ .../cloudflare/cloudflare-go/options.go | 90 +++ .../cloudflare/cloudflare-go/organizations.go | 185 ++++++ .../cloudflare/cloudflare-go/origin_ca.go | 168 +++++ .../cloudflare/cloudflare-go/page_rules.go | 206 ++++++ .../cloudflare/cloudflare-go/railgun.go | 297 +++++++++ .../cloudflare/cloudflare-go/rate_limiting.go | 195 ++++++ .../cloudflare/cloudflare-go/ssl.go | 148 +++++ .../cloudflare/cloudflare-go/user.go | 113 ++++ .../cloudflare/cloudflare-go/user_agent.go | 149 +++++ .../cloudflare/cloudflare-go/virtualdns.go | 125 ++++ .../cloudflare/cloudflare-go/waf.go | 97 +++ .../cloudflare/cloudflare-go/zone.go | 587 ++++++++++++++++++ vendor/golang.org/x/time/AUTHORS | 3 + vendor/golang.org/x/time/CONTRIBUTORS | 3 + vendor/golang.org/x/time/LICENSE | 27 + vendor/golang.org/x/time/PATENTS | 22 + vendor/golang.org/x/time/rate/rate.go | 384 ++++++++++++ vendor/golang.org/x/time/rate/rate_go16.go | 21 + vendor/golang.org/x/time/rate/rate_go17.go | 21 + 36 files changed, 4592 insertions(+), 451 deletions(-) delete mode 100644 providers/dns/cloudflare/client.go delete mode 100644 providers/dns/cloudflare/client_test.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/LICENSE create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cloudflare.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/cpage.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/custom_hostname.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/dns.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/errors.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/firewall.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/ips.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/keyless.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/load_balancing.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/lockdown.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/options.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/organizations.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/origin_ca.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/page_rules.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/railgun.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/rate_limiting.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/ssl.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/user.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/user_agent.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/virtualdns.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/waf.go create mode 100644 vendor/github.com/cloudflare/cloudflare-go/zone.go create mode 100644 vendor/golang.org/x/time/AUTHORS create mode 100644 vendor/golang.org/x/time/CONTRIBUTORS create mode 100644 vendor/golang.org/x/time/LICENSE create mode 100644 vendor/golang.org/x/time/PATENTS create mode 100644 vendor/golang.org/x/time/rate/rate.go create mode 100644 vendor/golang.org/x/time/rate/rate_go16.go create mode 100644 vendor/golang.org/x/time/rate/rate_go17.go 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/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.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/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.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/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() +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", + }, + } - config := NewDefaultConfig() - config.AuthEmail = "123" - config.AuthKey = "123" + 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) + } + } - _, err := NewDNSProviderConfig(config) + p, err := NewDNSProvider() - assert.NoError(t, err) + if len(test.expected) == 0 { + assert.NoError(t, err) + assert.NotNil(t, p) + } else { + require.EqualError(t, err, test.expected) + } + }) + } } -func TestNewDNSProviderValidEnv(t *testing.T) { - defer restoreEnv() - os.Setenv("CLOUDFLARE_EMAIL", "test@example.com") - os.Setenv("CLOUDFLARE_API_KEY", "123") +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.NoError(t, err) -} + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer restoreEnv() + os.Unsetenv("CLOUDFLARE_EMAIL") + os.Unsetenv("CLOUDFLARE_API_KEY") -func TestNewDNSProviderMissingCredErr(t *testing.T) { - defer restoreEnv() - os.Setenv("CLOUDFLARE_EMAIL", "") - os.Setenv("CLOUDFLARE_API_KEY", "") + config := NewDefaultConfig() + config.AuthEmail = test.authEmail + config.AuthKey = test.authKey - _, err := NewDNSProvider() - assert.EqualError(t, err, "cloudflare: some credentials information are missing: CLOUDFLARE_EMAIL,CLOUDFLARE_API_KEY") -} + p, err := NewDNSProviderConfig(config) -func TestNewDNSProviderMissingCredErrSingle(t *testing.T) { - defer restoreEnv() - os.Setenv("CLOUDFLARE_EMAIL", "awesome@possum.com") - - _, err := NewDNSProvider() - assert.EqualError(t, err, "cloudflare: some credentials information are missing: CLOUDFLARE_API_KEY") + 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) } 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) +}