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