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.
This commit is contained in:
Felix Lange 2019-09-25 11:38:13 +02:00 committed by GitHub
parent 32b07e8b1f
commit 0568e81701
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 10699 additions and 50 deletions

View File

@ -19,10 +19,10 @@ package main
import ( import (
"fmt" "fmt"
"net" "net"
"sort"
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
@ -38,24 +38,35 @@ var (
discv4PingCommand, discv4PingCommand,
discv4RequestRecordCommand, discv4RequestRecordCommand,
discv4ResolveCommand, discv4ResolveCommand,
discv4ResolveJSONCommand,
}, },
} }
discv4PingCommand = cli.Command{ discv4PingCommand = cli.Command{
Name: "ping", Name: "ping",
Usage: "Sends ping to a node", Usage: "Sends ping to a node",
Action: discv4Ping, Action: discv4Ping,
ArgsUsage: "<node>",
} }
discv4RequestRecordCommand = cli.Command{ discv4RequestRecordCommand = cli.Command{
Name: "requestenr", Name: "requestenr",
Usage: "Requests a node record using EIP-868 enrRequest", Usage: "Requests a node record using EIP-868 enrRequest",
Action: discv4RequestRecord, Action: discv4RequestRecord,
ArgsUsage: "<node>",
} }
discv4ResolveCommand = cli.Command{ discv4ResolveCommand = cli.Command{
Name: "resolve", Name: "resolve",
Usage: "Finds a node in the DHT", Usage: "Finds a node in the DHT",
Action: discv4Resolve, Action: discv4Resolve,
ArgsUsage: "<node>",
Flags: []cli.Flag{bootnodesFlag}, Flags: []cli.Flag{bootnodesFlag},
} }
discv4ResolveJSONCommand = cli.Command{
Name: "resolve-json",
Usage: "Re-resolves nodes in a nodes.json file",
Action: discv4ResolveJSON,
Flags: []cli.Flag{bootnodesFlag},
ArgsUsage: "<nodes.json file>",
}
) )
var bootnodesFlag = cli.StringFlag{ var bootnodesFlag = cli.StringFlag{
@ -64,10 +75,8 @@ var bootnodesFlag = cli.StringFlag{
} }
func discv4Ping(ctx *cli.Context) error { func discv4Ping(ctx *cli.Context) error {
n, disc, err := getNodeArgAndStartV4(ctx) n := getNodeArg(ctx)
if err != nil { disc := startV4(ctx)
return err
}
defer disc.Close() defer disc.Close()
start := time.Now() start := time.Now()
@ -79,10 +88,8 @@ func discv4Ping(ctx *cli.Context) error {
} }
func discv4RequestRecord(ctx *cli.Context) error { func discv4RequestRecord(ctx *cli.Context) error {
n, disc, err := getNodeArgAndStartV4(ctx) n := getNodeArg(ctx)
if err != nil { disc := startV4(ctx)
return err
}
defer disc.Close() defer disc.Close()
respN, err := disc.RequestENR(n) respN, err := disc.RequestENR(n)
@ -94,33 +101,43 @@ func discv4RequestRecord(ctx *cli.Context) error {
} }
func discv4Resolve(ctx *cli.Context) error { func discv4Resolve(ctx *cli.Context) error {
n, disc, err := getNodeArgAndStartV4(ctx) n := getNodeArg(ctx)
if err != nil { disc := startV4(ctx)
return err
}
defer disc.Close() defer disc.Close()
fmt.Println(disc.Resolve(n).String()) fmt.Println(disc.Resolve(n).String())
return nil return nil
} }
func getNodeArgAndStartV4(ctx *cli.Context) (*enode.Node, *discover.UDPv4, error) { func discv4ResolveJSON(ctx *cli.Context) error {
if ctx.NArg() != 1 { if ctx.NArg() < 1 {
return nil, nil, fmt.Errorf("missing node as command-line argument") return fmt.Errorf("need nodes file as argument")
} }
n, err := parseNode(ctx.Args()[0]) 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()
}
// Add nodes from command line arguments.
for i := 1; i < ctx.NArg(); i++ {
n, err := parseNode(ctx.Args().Get(i))
if err != nil { if err != nil {
return nil, nil, err exit(err)
} }
var bootnodes []*enode.Node nodes = append(nodes, n)
if commandHasFlag(ctx, bootnodesFlag) {
bootnodes, err = parseBootnodes(ctx)
if err != nil {
return nil, nil, err
} }
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) writeNodesJSON(file, result)
return n, disc, err return nil
} }
func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) { func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
@ -139,28 +156,39 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
return nodes, nil return nodes, nil
} }
// commandHasFlag returns true if the current command supports the given flag. // startV4 starts an ephemeral discovery V4 node.
func commandHasFlag(ctx *cli.Context, flag cli.Flag) bool { func startV4(ctx *cli.Context) *discover.UDPv4 {
flags := ctx.FlagNames() socket, ln, cfg, err := listen()
sort.Strings(flags) if err != nil {
i := sort.SearchStrings(flags, flag.GetName()) exit(err)
return i != len(flags) && flags[i] == flag.GetName() }
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 listen() (*net.UDPConn, *enode.LocalNode, discover.Config, error) {
func startV4(bootnodes []*enode.Node) (*discover.UDPv4, error) {
var cfg discover.Config var cfg discover.Config
cfg.Bootnodes = bootnodes
cfg.PrivateKey, _ = crypto.GenerateKey() cfg.PrivateKey, _ = crypto.GenerateKey()
db, _ := enode.OpenDB("") db, _ := enode.OpenDB("")
ln := enode.NewLocalNode(db, cfg.PrivateKey) ln := enode.NewLocalNode(db, cfg.PrivateKey)
socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{0, 0, 0, 0}}) socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{0, 0, 0, 0}})
if err != nil { if err != nil {
return nil, err db.Close()
return nil, nil, cfg, err
} }
addr := socket.LocalAddr().(*net.UDPAddr) addr := socket.LocalAddr().(*net.UDPAddr)
ln.SetFallbackIP(net.IP{127, 0, 0, 1}) ln.SetFallbackIP(net.IP{127, 0, 0, 1})
ln.SetFallbackUDP(addr.Port) ln.SetFallbackUDP(addr.Port)
return discover.ListenUDP(socket, ln, cfg) return socket, ln, cfg, nil
} }

View File

@ -0,0 +1,163 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"strings"
"github.com/cloudflare/cloudflare-go"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/dnsdisc"
"gopkg.in/urfave/cli.v1"
)
var (
cloudflareTokenFlag = cli.StringFlag{
Name: "token",
Usage: "CloudFlare API token",
EnvVar: "CLOUDFLARE_API_TOKEN",
}
cloudflareZoneIDFlag = cli.StringFlag{
Name: "zoneid",
Usage: "CloudFlare Zone ID (optional)",
}
)
type cloudflareClient struct {
*cloudflare.API
zoneID string
}
// newCloudflareClient sets up a CloudFlare API client from command line flags.
func newCloudflareClient(ctx *cli.Context) *cloudflareClient {
token := ctx.String(cloudflareTokenFlag.Name)
if token == "" {
exit(fmt.Errorf("need cloudflare API token to proceed"))
}
api, err := cloudflare.NewWithAPIToken(token)
if err != nil {
exit(fmt.Errorf("can't create Cloudflare client: %v", err))
}
return &cloudflareClient{
API: api,
zoneID: ctx.String(cloudflareZoneIDFlag.Name),
}
}
// deploy uploads the given tree to CloudFlare DNS.
func (c *cloudflareClient) deploy(name string, t *dnsdisc.Tree) error {
if err := c.checkZone(name); err != nil {
return err
}
records := t.ToTXT(name)
return c.uploadRecords(name, records)
}
// checkZone verifies permissions on the CloudFlare DNS Zone for name.
func (c *cloudflareClient) checkZone(name string) error {
if c.zoneID == "" {
log.Info(fmt.Sprintf("Finding CloudFlare zone ID for %s", name))
id, err := c.ZoneIDByName(name)
if err != nil {
return err
}
c.zoneID = id
}
log.Info(fmt.Sprintf("Checking Permissions on zone %s", c.zoneID))
zone, err := c.ZoneDetails(c.zoneID)
if err != nil {
return err
}
if !strings.HasSuffix(name, "."+zone.Name) {
return fmt.Errorf("CloudFlare zone name %q does not match name %q to be deployed", zone.Name, name)
}
needPerms := map[string]bool{"#zone:edit": false, "#zone:read": false}
for _, perm := range zone.Permissions {
if _, ok := needPerms[perm]; ok {
needPerms[perm] = true
}
}
for _, ok := range needPerms {
if !ok {
return fmt.Errorf("wrong permissions on zone %s: %v", c.zoneID, needPerms)
}
}
return nil
}
// uploadRecords updates the TXT records at a particular subdomain. All non-root records
// will have a TTL of "infinity" and all existing records not in the new map will be
// nuked!
func (c *cloudflareClient) uploadRecords(name string, records map[string]string) error {
// Convert all names to lowercase.
lrecords := make(map[string]string, len(records))
for name, r := range records {
lrecords[strings.ToLower(name)] = r
}
records = lrecords
log.Info(fmt.Sprintf("Retrieving existing TXT records on %s", name))
entries, err := c.DNSRecords(c.zoneID, cloudflare.DNSRecord{Type: "TXT"})
if err != nil {
return err
}
existing := make(map[string]cloudflare.DNSRecord)
for _, entry := range entries {
if !strings.HasSuffix(entry.Name, name) {
continue
}
existing[strings.ToLower(entry.Name)] = entry
}
// Iterate over the new records and inject anything missing.
for path, val := range records {
old, exists := existing[path]
if !exists {
// Entry is unknown, push a new one to Cloudflare.
log.Info(fmt.Sprintf("Creating %s = %q", path, val))
ttl := 1
if path != name {
ttl = 2147483647 // Max TTL permitted by Cloudflare
}
_, err = c.CreateDNSRecord(c.zoneID, cloudflare.DNSRecord{Type: "TXT", Name: path, Content: val, TTL: ttl})
} else if old.Content != val {
// Entry already exists, only change its content.
log.Info(fmt.Sprintf("Updating %s from %q to %q", path, old.Content, val))
old.Content = val
err = c.UpdateDNSRecord(c.zoneID, old.ID, old)
} else {
log.Info(fmt.Sprintf("Skipping %s = %q", path, val))
}
if err != nil {
return fmt.Errorf("failed to publish %s: %v", path, err)
}
}
// Iterate over the old records and delete anything stale.
for path, entry := range existing {
if _, ok := records[path]; ok {
continue
}
// Stale entry, nuke it.
log.Info(fmt.Sprintf("Deleting %s = %q", path, entry.Content))
if err := c.DeleteDNSRecord(c.zoneID, entry.ID); err != nil {
return fmt.Errorf("failed to delete %s: %v", path, err)
}
}
return nil
}

358
cmd/devp2p/dnscmd.go Normal file
View File

@ -0,0 +1,358 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"crypto/ecdsa"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console"
"github.com/ethereum/go-ethereum/p2p/dnsdisc"
"github.com/ethereum/go-ethereum/p2p/enode"
cli "gopkg.in/urfave/cli.v1"
)
var (
dnsCommand = cli.Command{
Name: "dns",
Usage: "DNS Discovery Commands",
Subcommands: []cli.Command{
dnsSyncCommand,
dnsSignCommand,
dnsTXTCommand,
dnsCloudflareCommand,
},
}
dnsSyncCommand = cli.Command{
Name: "sync",
Usage: "Download a DNS discovery tree",
ArgsUsage: "<url> [ <directory> ]",
Action: dnsSync,
Flags: []cli.Flag{dnsTimeoutFlag},
}
dnsSignCommand = cli.Command{
Name: "sign",
Usage: "Sign a DNS discovery tree",
ArgsUsage: "<tree-directory> <key-file>",
Action: dnsSign,
Flags: []cli.Flag{dnsDomainFlag, dnsSeqFlag},
}
dnsTXTCommand = cli.Command{
Name: "to-txt",
Usage: "Create a DNS TXT records for a discovery tree",
ArgsUsage: "<tree-directory> <output-file>",
Action: dnsToTXT,
}
dnsCloudflareCommand = cli.Command{
Name: "to-cloudflare",
Usage: "Deploy DNS TXT records to cloudflare",
ArgsUsage: "<tree-directory>",
Action: dnsToCloudflare,
Flags: []cli.Flag{cloudflareTokenFlag, cloudflareZoneIDFlag},
}
)
var (
dnsTimeoutFlag = cli.DurationFlag{
Name: "timeout",
Usage: "Timeout for DNS lookups",
}
dnsDomainFlag = cli.StringFlag{
Name: "domain",
Usage: "Domain name of the tree",
}
dnsSeqFlag = cli.UintFlag{
Name: "seq",
Usage: "New sequence number of the tree",
}
)
// dnsSync performs dnsSyncCommand.
func dnsSync(ctx *cli.Context) error {
var (
c = dnsClient(ctx)
url = ctx.Args().Get(0)
outdir = ctx.Args().Get(1)
)
domain, _, err := dnsdisc.ParseURL(url)
if err != nil {
return err
}
if outdir == "" {
outdir = domain
}
t, err := c.SyncTree(url)
if err != nil {
return err
}
def := treeToDefinition(url, t)
def.Meta.LastModified = time.Now()
writeTreeDefinition(outdir, def)
return nil
}
func dnsSign(ctx *cli.Context) error {
if ctx.NArg() < 2 {
return fmt.Errorf("need tree definition directory and key file as arguments")
}
var (
defdir = ctx.Args().Get(0)
keyfile = ctx.Args().Get(1)
def = loadTreeDefinition(defdir)
domain = directoryName(defdir)
)
if def.Meta.URL != "" {
d, _, err := dnsdisc.ParseURL(def.Meta.URL)
if err != nil {
return fmt.Errorf("invalid 'url' field: %v", err)
}
domain = d
}
if ctx.IsSet(dnsDomainFlag.Name) {
domain = ctx.String(dnsDomainFlag.Name)
}
if ctx.IsSet(dnsSeqFlag.Name) {
def.Meta.Seq = ctx.Uint(dnsSeqFlag.Name)
} else {
def.Meta.Seq++ // Auto-bump sequence number if not supplied via flag.
}
t, err := dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links)
if err != nil {
return err
}
key := loadSigningKey(keyfile)
url, err := t.Sign(key, domain)
if err != nil {
return fmt.Errorf("can't sign: %v", err)
}
def = treeToDefinition(url, t)
def.Meta.LastModified = time.Now()
writeTreeDefinition(defdir, def)
return nil
}
func directoryName(dir string) string {
abs, err := filepath.Abs(dir)
if err != nil {
exit(err)
}
return filepath.Base(abs)
}
// dnsToTXT peforms dnsTXTCommand.
func dnsToTXT(ctx *cli.Context) error {
if ctx.NArg() < 1 {
return fmt.Errorf("need tree definition directory as argument")
}
output := ctx.Args().Get(1)
if output == "" {
output = "-" // default to stdout
}
domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
if err != nil {
return err
}
writeTXTJSON(output, t.ToTXT(domain))
return nil
}
// dnsToCloudflare peforms dnsCloudflareCommand.
func dnsToCloudflare(ctx *cli.Context) error {
if ctx.NArg() < 1 {
return fmt.Errorf("need tree definition directory as argument")
}
domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
if err != nil {
return err
}
client := newCloudflareClient(ctx)
return client.deploy(domain, t)
}
// loadSigningKey loads a private key in Ethereum keystore format.
func loadSigningKey(keyfile string) *ecdsa.PrivateKey {
keyjson, err := ioutil.ReadFile(keyfile)
if err != nil {
exit(fmt.Errorf("failed to read the keyfile at '%s': %v", keyfile, err))
}
password, _ := console.Stdin.PromptPassword("Please enter the password for '" + keyfile + "': ")
key, err := keystore.DecryptKey(keyjson, password)
if err != nil {
exit(fmt.Errorf("error decrypting key: %v", err))
}
return key.PrivateKey
}
// dnsClient configures the DNS discovery client from command line flags.
func dnsClient(ctx *cli.Context) *dnsdisc.Client {
var cfg dnsdisc.Config
if commandHasFlag(ctx, dnsTimeoutFlag) {
cfg.Timeout = ctx.Duration(dnsTimeoutFlag.Name)
}
c, _ := dnsdisc.NewClient(cfg) // cannot fail because no URLs given
return c
}
// There are two file formats for DNS node trees on disk:
//
// The 'TXT' format is a single JSON file containing DNS TXT records
// as a JSON object where the keys are names and the values are objects
// containing the value of the record.
//
// The 'definition' format is a directory containing two files:
//
// enrtree-info.json -- contains sequence number & links to other trees
// nodes.json -- contains the nodes as a JSON array.
//
// This format exists because it's convenient to edit. nodes.json can be generated
// in multiple ways: it may be written by a DHT crawler or compiled by a human.
type dnsDefinition struct {
Meta dnsMetaJSON
Nodes []*enode.Node
}
type dnsMetaJSON struct {
URL string `json:"url,omitempty"`
Seq uint `json:"seq"`
Sig string `json:"signature,omitempty"`
Links []string `json:"links"`
LastModified time.Time `json:"lastModified"`
}
func treeToDefinition(url string, t *dnsdisc.Tree) *dnsDefinition {
meta := dnsMetaJSON{
URL: url,
Seq: t.Seq(),
Sig: t.Signature(),
Links: t.Links(),
}
if meta.Links == nil {
meta.Links = []string{}
}
return &dnsDefinition{Meta: meta, Nodes: t.Nodes()}
}
// loadTreeDefinition loads a directory in 'definition' format.
func loadTreeDefinition(directory string) *dnsDefinition {
metaFile, nodesFile := treeDefinitionFiles(directory)
var def dnsDefinition
err := common.LoadJSON(metaFile, &def.Meta)
if err != nil && !os.IsNotExist(err) {
exit(err)
}
if def.Meta.Links == nil {
def.Meta.Links = []string{}
}
// Check link syntax.
for _, link := range def.Meta.Links {
if _, _, err := dnsdisc.ParseURL(link); err != nil {
exit(fmt.Errorf("invalid link %q: %v", link, err))
}
}
// Check/convert nodes.
nodes := loadNodesJSON(nodesFile)
if err := nodes.verify(); err != nil {
exit(err)
}
def.Nodes = nodes.nodes()
return &def
}
// loadTreeDefinitionForExport loads a DNS tree and ensures it is signed.
func loadTreeDefinitionForExport(dir string) (domain string, t *dnsdisc.Tree, err error) {
metaFile, _ := treeDefinitionFiles(dir)
def := loadTreeDefinition(dir)
if def.Meta.URL == "" {
return "", nil, fmt.Errorf("missing 'url' field in %v", metaFile)
}
domain, pubkey, err := dnsdisc.ParseURL(def.Meta.URL)
if err != nil {
return "", nil, fmt.Errorf("invalid 'url' field in %v: %v", metaFile, err)
}
if t, err = dnsdisc.MakeTree(def.Meta.Seq, def.Nodes, def.Meta.Links); err != nil {
return "", nil, err
}
if err := ensureValidTreeSignature(t, pubkey, def.Meta.Sig); err != nil {
return "", nil, err
}
return domain, t, nil
}
// ensureValidTreeSignature checks that sig is valid for tree and assigns it as the
// tree's signature if valid.
func ensureValidTreeSignature(t *dnsdisc.Tree, pubkey *ecdsa.PublicKey, sig string) error {
if sig == "" {
return fmt.Errorf("missing signature, run 'devp2p dns sign' first")
}
if err := t.SetSignature(pubkey, sig); err != nil {
return fmt.Errorf("invalid signature on tree, run 'devp2p dns sign' to update it")
}
return nil
}
// writeTreeDefinition writes a DNS node tree definition to the given directory.
func writeTreeDefinition(directory string, def *dnsDefinition) {
metaJSON, err := json.MarshalIndent(&def.Meta, "", jsonIndent)
if err != nil {
exit(err)
}
// Convert nodes.
nodes := make(nodeSet, len(def.Nodes))
nodes.add(def.Nodes...)
// Write.
if err := os.Mkdir(directory, 0744); err != nil && !os.IsExist(err) {
exit(err)
}
metaFile, nodesFile := treeDefinitionFiles(directory)
writeNodesJSON(nodesFile, nodes)
if err := ioutil.WriteFile(metaFile, metaJSON, 0644); err != nil {
exit(err)
}
}
func treeDefinitionFiles(directory string) (string, string) {
meta := filepath.Join(directory, "enrtree-info.json")
nodes := filepath.Join(directory, "nodes.json")
return meta, nodes
}
// writeTXTJSON writes TXT records in JSON format.
func writeTXTJSON(file string, txt map[string]string) {
txtJSON, err := json.MarshalIndent(txt, "", jsonIndent)
if err != nil {
exit(err)
}
if file == "-" {
os.Stdout.Write(txtJSON)
fmt.Println()
return
}
if err := ioutil.WriteFile(file, txtJSON, 0644); err != nil {
exit(err)
}
}

View File

@ -20,8 +20,10 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
) )
@ -57,12 +59,38 @@ func init() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
enrdumpCommand, enrdumpCommand,
discv4Command, discv4Command,
dnsCommand,
} }
} }
func main() { func main() {
if err := app.Run(os.Args); err != nil { 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) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
}
} }

87
cmd/devp2p/nodeset.go Normal file
View File

@ -0,0 +1,87 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/p2p/enode"
)
const jsonIndent = " "
// nodeSet is the nodes.json file format. It holds a set of node records
// as a JSON object.
type nodeSet map[enode.ID]nodeJSON
type nodeJSON struct {
Seq uint64 `json:"seq"`
N *enode.Node `json:"record"`
}
func loadNodesJSON(file string) nodeSet {
var nodes nodeSet
if err := common.LoadJSON(file, &nodes); err != nil {
exit(err)
}
return nodes
}
func writeNodesJSON(file string, nodes nodeSet) {
nodesJSON, err := json.MarshalIndent(nodes, "", jsonIndent)
if err != nil {
exit(err)
}
if err := ioutil.WriteFile(file, nodesJSON, 0644); err != nil {
exit(err)
}
}
func (ns nodeSet) nodes() []*enode.Node {
result := make([]*enode.Node, 0, len(ns))
for _, n := range ns {
result = append(result, n.N)
}
// Sort by ID.
sort.Slice(result, func(i, j int) bool {
return bytes.Compare(result[i].ID().Bytes(), result[j].ID().Bytes()) < 0
})
return result
}
func (ns nodeSet) add(nodes ...*enode.Node) {
for _, n := range nodes {
ns[n.ID()] = nodeJSON{Seq: n.Seq(), N: n}
}
}
func (ns nodeSet) verify() error {
for id, n := range ns {
if n.N.ID() != id {
return fmt.Errorf("invalid node %v: ID does not match ID %v in record", id, n.N.ID())
}
if n.N.Seq() != n.Seq {
return fmt.Errorf("invalid node %v: 'seq' does not match seq %d from record", id, n.N.Seq())
}
}
return nil
}

260
p2p/dnsdisc/client.go Normal file
View File

@ -0,0 +1,260 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dnsdisc
import (
"bytes"
"context"
"fmt"
"math/rand"
"net"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
lru "github.com/hashicorp/golang-lru"
)
// Client discovers nodes by querying DNS servers.
type Client struct {
cfg Config
clock mclock.Clock
linkCache linkCache
trees map[string]*clientTree
entries *lru.Cache
}
// Config holds configuration options for the client.
type Config struct {
Timeout time.Duration // timeout used for DNS lookups (default 5s)
RecheckInterval time.Duration // time between tree root update checks (default 30min)
CacheLimit int // maximum number of cached records (default 1000)
ValidSchemes enr.IdentityScheme // acceptable ENR identity schemes (default enode.ValidSchemes)
Resolver Resolver // the DNS resolver to use (defaults to system DNS)
Logger log.Logger // destination of client log messages (defaults to root logger)
}
// Resolver is a DNS resolver that can query TXT records.
type Resolver interface {
LookupTXT(ctx context.Context, domain string) ([]string, error)
}
func (cfg Config) withDefaults() Config {
const (
defaultTimeout = 5 * time.Second
defaultRecheck = 30 * time.Minute
defaultCache = 1000
)
if cfg.Timeout == 0 {
cfg.Timeout = defaultTimeout
}
if cfg.RecheckInterval == 0 {
cfg.RecheckInterval = defaultRecheck
}
if cfg.CacheLimit == 0 {
cfg.CacheLimit = defaultCache
}
if cfg.ValidSchemes == nil {
cfg.ValidSchemes = enode.ValidSchemes
}
if cfg.Resolver == nil {
cfg.Resolver = new(net.Resolver)
}
if cfg.Logger == nil {
cfg.Logger = log.Root()
}
return cfg
}
// NewClient creates a client.
func NewClient(cfg Config, urls ...string) (*Client, error) {
c := &Client{
cfg: cfg.withDefaults(),
clock: mclock.System{},
trees: make(map[string]*clientTree),
}
var err error
if c.entries, err = lru.New(c.cfg.CacheLimit); err != nil {
return nil, err
}
for _, url := range urls {
if err := c.AddTree(url); err != nil {
return nil, err
}
}
return c, nil
}
// SyncTree downloads the entire node tree at the given URL. This doesn't add the tree for
// later use, but any previously-synced entries are reused.
func (c *Client) SyncTree(url string) (*Tree, error) {
le, err := parseURL(url)
if err != nil {
return nil, fmt.Errorf("invalid enrtree URL: %v", err)
}
ct := newClientTree(c, le)
t := &Tree{entries: make(map[string]entry)}
if err := ct.syncAll(t.entries); err != nil {
return nil, err
}
t.root = ct.root
return t, nil
}
// AddTree adds a enrtree:// URL to crawl.
func (c *Client) AddTree(url string) error {
le, err := parseURL(url)
if err != nil {
return fmt.Errorf("invalid enrtree URL: %v", err)
}
ct, err := c.ensureTree(le)
if err != nil {
return err
}
c.linkCache.add(ct)
return nil
}
func (c *Client) ensureTree(le *linkEntry) (*clientTree, error) {
if tree, ok := c.trees[le.domain]; ok {
if !tree.matchPubkey(le.pubkey) {
return nil, fmt.Errorf("conflicting public keys for domain %q", le.domain)
}
return tree, nil
}
ct := newClientTree(c, le)
c.trees[le.domain] = ct
return ct, nil
}
// RandomNode retrieves the next random node.
func (c *Client) RandomNode(ctx context.Context) *enode.Node {
for {
ct := c.randomTree()
if ct == nil {
return nil
}
n, err := ct.syncRandom(ctx)
if err != nil {
if err == ctx.Err() {
return nil // context canceled.
}
c.cfg.Logger.Debug("Error in DNS random node sync", "tree", ct.loc.domain, "err", err)
continue
}
if n != nil {
return n
}
}
}
// randomTree returns a random tree.
func (c *Client) randomTree() *clientTree {
if !c.linkCache.valid() {
c.gcTrees()
}
limit := rand.Intn(len(c.trees))
for _, ct := range c.trees {
if limit == 0 {
return ct
}
limit--
}
return nil
}
// gcTrees rebuilds the 'trees' map.
func (c *Client) gcTrees() {
trees := make(map[string]*clientTree)
for t := range c.linkCache.all() {
trees[t.loc.domain] = t
}
c.trees = trees
}
// resolveRoot retrieves a root entry via DNS.
func (c *Client) resolveRoot(ctx context.Context, loc *linkEntry) (rootEntry, error) {
txts, err := c.cfg.Resolver.LookupTXT(ctx, loc.domain)
c.cfg.Logger.Trace("Updating DNS discovery root", "tree", loc.domain, "err", err)
if err != nil {
return rootEntry{}, err
}
for _, txt := range txts {
if strings.HasPrefix(txt, rootPrefix) {
return parseAndVerifyRoot(txt, loc)
}
}
return rootEntry{}, nameError{loc.domain, errNoRoot}
}
func parseAndVerifyRoot(txt string, loc *linkEntry) (rootEntry, error) {
e, err := parseRoot(txt)
if err != nil {
return e, err
}
if !e.verifySignature(loc.pubkey) {
return e, entryError{typ: "root", err: errInvalidSig}
}
return e, nil
}
// resolveEntry retrieves an entry from the cache or fetches it from the network
// if it isn't cached.
func (c *Client) resolveEntry(ctx context.Context, domain, hash string) (entry, error) {
cacheKey := truncateHash(hash)
if e, ok := c.entries.Get(cacheKey); ok {
return e.(entry), nil
}
e, err := c.doResolveEntry(ctx, domain, hash)
if err != nil {
return nil, err
}
c.entries.Add(cacheKey, e)
return e, nil
}
// doResolveEntry fetches an entry via DNS.
func (c *Client) doResolveEntry(ctx context.Context, domain, hash string) (entry, error) {
wantHash, err := b32format.DecodeString(hash)
if err != nil {
return nil, fmt.Errorf("invalid base32 hash")
}
name := hash + "." + domain
txts, err := c.cfg.Resolver.LookupTXT(ctx, hash+"."+domain)
c.cfg.Logger.Trace("DNS discovery lookup", "name", name, "err", err)
if err != nil {
return nil, err
}
for _, txt := range txts {
e, err := parseEntry(txt, c.cfg.ValidSchemes)
if err == errUnknownEntry {
continue
}
if !bytes.HasPrefix(crypto.Keccak256([]byte(txt)), wantHash) {
err = nameError{name, errHashMismatch}
} else if err != nil {
err = nameError{name, err}
}
return e, err
}
return nil, nameError{name, errNoEntry}
}

306
p2p/dnsdisc/client_test.go Normal file
View File

@ -0,0 +1,306 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dnsdisc
import (
"context"
"crypto/ecdsa"
"math/rand"
"reflect"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
)
const (
signingKeySeed = 0x111111
nodesSeed1 = 0x2945237
nodesSeed2 = 0x4567299
)
func TestClientSyncTree(t *testing.T) {
r := mapResolver{
"3CA2MBMUQ55ZCT74YEEQLANJDI.n": "enr=-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI=",
"53HBTPGGZ4I76UEPCNQGZWIPTQ.n": "enr=-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA=",
"BG7SVUBUAJ3UAWD2ATEBLMRNEE.n": "enrtree=53HBTPGGZ4I76UEPCNQGZWIPTQ,3CA2MBMUQ55ZCT74YEEQLANJDI,HNHR6UTVZF5TJKK3FV27ZI76P4",
"HNHR6UTVZF5TJKK3FV27ZI76P4.n": "enr=-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o=",
"JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
"n": "enrtree-root=v1 e=BG7SVUBUAJ3UAWD2ATEBLMRNEE l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=1 sig=gacuU0nTy9duIdu1IFDyF5Lv9CFHqHiNcj91n0frw70tZo3tZZsCVkE3j1ILYyVOHRLWGBmawo_SEkThZ9PgcQE=",
}
var (
wantNodes = testNodes(0x29452, 3)
wantLinks = []string{"enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org"}
wantSeq = uint(1)
)
c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
stree, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
if err != nil {
t.Fatal("sync error:", err)
}
if !reflect.DeepEqual(sortByID(stree.Nodes()), sortByID(wantNodes)) {
t.Errorf("wrong nodes in synced tree:\nhave %v\nwant %v", spew.Sdump(stree.Nodes()), spew.Sdump(wantNodes))
}
if !reflect.DeepEqual(stree.Links(), wantLinks) {
t.Errorf("wrong links in synced tree: %v", stree.Links())
}
if stree.Seq() != wantSeq {
t.Errorf("synced tree has wrong seq: %d", stree.Seq())
}
if len(c.trees) > 0 {
t.Errorf("tree from SyncTree added to client")
}
}
// In this test, syncing the tree fails because it contains an invalid ENR entry.
func TestClientSyncTreeBadNode(t *testing.T) {
r := mapResolver{
"n": "enrtree-root=v1 e=ZFJZDQKSOMJRYYQSZKJZC54HCF l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=WEy8JTZ2dHmXM2qeBZ7D2ECK7SGbnurl1ge_S_5GQBAqnADk0gLTcg8Lm5QNqLHZjJKGAb443p996idlMcBqEQA=",
"JGUFMSAGI7KZYB3P7IZW4S5Y3A.n": "enrtree-link=AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
"ZFJZDQKSOMJRYYQSZKJZC54HCF.n": "enr=gggggggggggggg=",
}
c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
_, err := c.SyncTree("enrtree://APFGGTFOBVE2ZNAB3CSMNNX6RRK3ODIRLP2AA5U4YFAA6MSYZUYTQ@n")
wantErr := nameError{name: "ZFJZDQKSOMJRYYQSZKJZC54HCF.n", err: entryError{typ: "enr", err: errInvalidENR}}
if err != wantErr {
t.Fatalf("expected sync error %q, got %q", wantErr, err)
}
}
// This test checks that RandomNode hits all entries.
func TestClientRandomNode(t *testing.T) {
nodes := testNodes(nodesSeed1, 30)
tree, url := makeTestTree("n", nodes, nil)
r := mapResolver(tree.ToTXT("n"))
c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
if err := c.AddTree(url); err != nil {
t.Fatal(err)
}
checkRandomNode(t, c, nodes)
}
// This test checks that RandomNode traverses linked trees as well as explicitly added trees.
func TestClientRandomNodeLinks(t *testing.T) {
nodes := testNodes(nodesSeed1, 40)
tree1, url1 := makeTestTree("t1", nodes[:10], nil)
tree2, url2 := makeTestTree("t2", nodes[10:], []string{url1})
cfg := Config{
Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")),
Logger: testlog.Logger(t, log.LvlTrace),
}
c, _ := NewClient(cfg)
if err := c.AddTree(url2); err != nil {
t.Fatal(err)
}
checkRandomNode(t, c, nodes)
}
// This test verifies that RandomNode re-checks the root of the tree to catch
// updates to nodes.
func TestClientRandomNodeUpdates(t *testing.T) {
var (
clock = new(mclock.Simulated)
nodes = testNodes(nodesSeed1, 30)
resolver = newMapResolver()
cfg = Config{
Resolver: resolver,
Logger: testlog.Logger(t, log.LvlTrace),
RecheckInterval: 20 * time.Minute,
}
c, _ = NewClient(cfg)
)
c.clock = clock
tree1, url := makeTestTree("n", nodes[:25], nil)
// Sync the original tree.
resolver.add(tree1.ToTXT("n"))
c.AddTree(url)
checkRandomNode(t, c, nodes[:25])
// Update some nodes and ensure RandomNode returns the new nodes as well.
keys := testKeys(nodesSeed1, len(nodes))
for i, n := range nodes[:len(nodes)/2] {
r := n.Record()
r.Set(enr.IP{127, 0, 0, 1})
r.SetSeq(55)
enode.SignV4(r, keys[i])
n2, _ := enode.New(enode.ValidSchemes, r)
nodes[i] = n2
}
tree2, _ := makeTestTree("n", nodes, nil)
clock.Run(cfg.RecheckInterval + 1*time.Second)
resolver.clear()
resolver.add(tree2.ToTXT("n"))
checkRandomNode(t, c, nodes)
}
// This test verifies that RandomNode re-checks the root of the tree to catch
// updates to links.
func TestClientRandomNodeLinkUpdates(t *testing.T) {
var (
clock = new(mclock.Simulated)
nodes = testNodes(nodesSeed1, 30)
resolver = newMapResolver()
cfg = Config{
Resolver: resolver,
Logger: testlog.Logger(t, log.LvlTrace),
RecheckInterval: 20 * time.Minute,
}
c, _ = NewClient(cfg)
)
c.clock = clock
tree3, url3 := makeTestTree("t3", nodes[20:30], nil)
tree2, url2 := makeTestTree("t2", nodes[10:20], nil)
tree1, url1 := makeTestTree("t1", nodes[0:10], []string{url2})
resolver.add(tree1.ToTXT("t1"))
resolver.add(tree2.ToTXT("t2"))
resolver.add(tree3.ToTXT("t3"))
// Sync tree1 using RandomNode.
c.AddTree(url1)
checkRandomNode(t, c, nodes[:20])
// Add link to tree3, remove link to tree2.
tree1, _ = makeTestTree("t1", nodes[:10], []string{url3})
resolver.add(tree1.ToTXT("t1"))
clock.Run(cfg.RecheckInterval + 1*time.Second)
t.Log("tree1 updated")
var wantNodes []*enode.Node
wantNodes = append(wantNodes, tree1.Nodes()...)
wantNodes = append(wantNodes, tree3.Nodes()...)
checkRandomNode(t, c, wantNodes)
// Check that linked trees are GCed when they're no longer referenced.
if len(c.trees) != 2 {
t.Errorf("client knows %d trees, want 2", len(c.trees))
}
}
func checkRandomNode(t *testing.T, c *Client, wantNodes []*enode.Node) {
t.Helper()
var (
want = make(map[enode.ID]*enode.Node)
maxCalls = len(wantNodes) * 2
calls = 0
ctx = context.Background()
)
for _, n := range wantNodes {
want[n.ID()] = n
}
for ; len(want) > 0 && calls < maxCalls; calls++ {
n := c.RandomNode(ctx)
if n == nil {
t.Fatalf("RandomNode returned nil (call %d)", calls)
}
delete(want, n.ID())
}
t.Logf("checkRandomNode called RandomNode %d times to find %d nodes", calls, len(wantNodes))
for _, n := range want {
t.Errorf("RandomNode didn't discover node %v", n.ID())
}
}
func makeTestTree(domain string, nodes []*enode.Node, links []string) (*Tree, string) {
tree, err := MakeTree(1, nodes, links)
if err != nil {
panic(err)
}
url, err := tree.Sign(testKey(signingKeySeed), domain)
if err != nil {
panic(err)
}
return tree, url
}
// testKeys creates deterministic private keys for testing.
func testKeys(seed int64, n int) []*ecdsa.PrivateKey {
rand := rand.New(rand.NewSource(seed))
keys := make([]*ecdsa.PrivateKey, n)
for i := 0; i < n; i++ {
key, err := ecdsa.GenerateKey(crypto.S256(), rand)
if err != nil {
panic("can't generate key: " + err.Error())
}
keys[i] = key
}
return keys
}
func testKey(seed int64) *ecdsa.PrivateKey {
return testKeys(seed, 1)[0]
}
func testNodes(seed int64, n int) []*enode.Node {
keys := testKeys(seed, n)
nodes := make([]*enode.Node, n)
for i, key := range keys {
record := new(enr.Record)
record.SetSeq(uint64(i))
enode.SignV4(record, key)
n, err := enode.New(enode.ValidSchemes, record)
if err != nil {
panic(err)
}
nodes[i] = n
}
return nodes
}
func testNode(seed int64) *enode.Node {
return testNodes(seed, 1)[0]
}
type mapResolver map[string]string
func newMapResolver(maps ...map[string]string) mapResolver {
mr := make(mapResolver)
for _, m := range maps {
mr.add(m)
}
return mr
}
func (mr mapResolver) clear() {
for k := range mr {
delete(mr, k)
}
}
func (mr mapResolver) add(m map[string]string) {
for k, v := range m {
mr[k] = v
}
}
func (mr mapResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
if record, ok := mr[name]; ok {
return []string{record}, nil
}
return nil, nil
}

18
p2p/dnsdisc/doc.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Package dnsdisc implements node discovery via DNS (EIP-1459).
package dnsdisc

63
p2p/dnsdisc/error.go Normal file
View File

@ -0,0 +1,63 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dnsdisc
import (
"errors"
"fmt"
)
// Entry parse errors.
var (
errUnknownEntry = errors.New("unknown entry type")
errNoPubkey = errors.New("missing public key")
errBadPubkey = errors.New("invalid public key")
errInvalidENR = errors.New("invalid node record")
errInvalidChild = errors.New("invalid child hash")
errInvalidSig = errors.New("invalid base64 signature")
errSyntax = errors.New("invalid syntax")
)
// Resolver/sync errors
var (
errNoRoot = errors.New("no valid root found")
errNoEntry = errors.New("no valid tree entry found")
errHashMismatch = errors.New("hash mismatch")
errENRInLinkTree = errors.New("enr entry in link tree")
errLinkInENRTree = errors.New("link entry in ENR tree")
)
type nameError struct {
name string
err error
}
func (err nameError) Error() string {
if ee, ok := err.err.(entryError); ok {
return fmt.Sprintf("invalid %s entry at %s: %v", ee.typ, err.name, ee.err)
}
return err.name + ": " + err.err.Error()
}
type entryError struct {
typ string
err error
}
func (err entryError) Error() string {
return fmt.Sprintf("invalid %s entry: %v", err.typ, err.err)
}

277
p2p/dnsdisc/sync.go Normal file
View File

@ -0,0 +1,277 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dnsdisc
import (
"context"
"crypto/ecdsa"
"math/rand"
"time"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/p2p/enode"
)
// clientTree is a full tree being synced.
type clientTree struct {
c *Client
loc *linkEntry
root *rootEntry
lastRootCheck mclock.AbsTime // last revalidation of root
enrs *subtreeSync
links *subtreeSync
linkCache linkCache
}
func newClientTree(c *Client, loc *linkEntry) *clientTree {
ct := &clientTree{c: c, loc: loc}
ct.linkCache.self = ct
return ct
}
func (ct *clientTree) matchPubkey(key *ecdsa.PublicKey) bool {
return keysEqual(ct.loc.pubkey, key)
}
func keysEqual(k1, k2 *ecdsa.PublicKey) bool {
return k1.Curve == k2.Curve && k1.X.Cmp(k2.X) == 0 && k1.Y.Cmp(k2.Y) == 0
}
// syncAll retrieves all entries of the tree.
func (ct *clientTree) syncAll(dest map[string]entry) error {
if err := ct.updateRoot(); err != nil {
return err
}
if err := ct.links.resolveAll(dest); err != nil {
return err
}
if err := ct.enrs.resolveAll(dest); err != nil {
return err
}
return nil
}
// syncRandom retrieves a single entry of the tree. The Node return value
// is non-nil if the entry was a node.
func (ct *clientTree) syncRandom(ctx context.Context) (*enode.Node, error) {
if ct.rootUpdateDue() {
if err := ct.updateRoot(); err != nil {
return nil, err
}
}
// Link tree sync has priority, run it to completion before syncing ENRs.
if !ct.links.done() {
err := ct.syncNextLink(ctx)
return nil, err
}
// Sync next random entry in ENR tree. Once every node has been visited, we simply
// start over. This is fine because entries are cached.
if ct.enrs.done() {
ct.enrs = newSubtreeSync(ct.c, ct.loc, ct.root.eroot, false)
}
return ct.syncNextRandomENR(ctx)
}
func (ct *clientTree) syncNextLink(ctx context.Context) error {
hash := ct.links.missing[0]
e, err := ct.links.resolveNext(ctx, hash)
if err != nil {
return err
}
ct.links.missing = ct.links.missing[1:]
if le, ok := e.(*linkEntry); ok {
lt, err := ct.c.ensureTree(le)
if err != nil {
return err
}
ct.linkCache.add(lt)
}
return nil
}
func (ct *clientTree) syncNextRandomENR(ctx context.Context) (*enode.Node, error) {
index := rand.Intn(len(ct.enrs.missing))
hash := ct.enrs.missing[index]
e, err := ct.enrs.resolveNext(ctx, hash)
if err != nil {
return nil, err
}
ct.enrs.missing = removeHash(ct.enrs.missing, index)
if ee, ok := e.(*enrEntry); ok {
return ee.node, nil
}
return nil, nil
}
func (ct *clientTree) String() string {
return ct.loc.url()
}
// removeHash removes the element at index from h.
func removeHash(h []string, index int) []string {
if len(h) == 1 {
return nil
}
last := len(h) - 1
if index < last {
h[index] = h[last]
h[last] = ""
}
return h[:last]
}
// updateRoot ensures that the given tree has an up-to-date root.
func (ct *clientTree) updateRoot() error {
ct.lastRootCheck = ct.c.clock.Now()
ctx, cancel := context.WithTimeout(context.Background(), ct.c.cfg.Timeout)
defer cancel()
root, err := ct.c.resolveRoot(ctx, ct.loc)
if err != nil {
return err
}
ct.root = &root
// Invalidate subtrees if changed.
if ct.links == nil || root.lroot != ct.links.root {
ct.links = newSubtreeSync(ct.c, ct.loc, root.lroot, true)
ct.linkCache.reset()
}
if ct.enrs == nil || root.eroot != ct.enrs.root {
ct.enrs = newSubtreeSync(ct.c, ct.loc, root.eroot, false)
}
return nil
}
// rootUpdateDue returns true when a root update is needed.
func (ct *clientTree) rootUpdateDue() bool {
return ct.root == nil || time.Duration(ct.c.clock.Now()-ct.lastRootCheck) > ct.c.cfg.RecheckInterval
}
// subtreeSync is the sync of an ENR or link subtree.
type subtreeSync struct {
c *Client
loc *linkEntry
root string
missing []string // missing tree node hashes
link bool // true if this sync is for the link tree
}
func newSubtreeSync(c *Client, loc *linkEntry, root string, link bool) *subtreeSync {
return &subtreeSync{c, loc, root, []string{root}, link}
}
func (ts *subtreeSync) done() bool {
return len(ts.missing) == 0
}
func (ts *subtreeSync) resolveAll(dest map[string]entry) error {
for !ts.done() {
hash := ts.missing[0]
ctx, cancel := context.WithTimeout(context.Background(), ts.c.cfg.Timeout)
e, err := ts.resolveNext(ctx, hash)
cancel()
if err != nil {
return err
}
dest[hash] = e
ts.missing = ts.missing[1:]
}
return nil
}
func (ts *subtreeSync) resolveNext(ctx context.Context, hash string) (entry, error) {
e, err := ts.c.resolveEntry(ctx, ts.loc.domain, hash)
if err != nil {
return nil, err
}
switch e := e.(type) {
case *enrEntry:
if ts.link {
return nil, errENRInLinkTree
}
case *linkEntry:
if !ts.link {
return nil, errLinkInENRTree
}
case *subtreeEntry:
ts.missing = append(ts.missing, e.children...)
}
return e, nil
}
// linkCache tracks the links of a tree.
type linkCache struct {
self *clientTree
directM map[*clientTree]struct{} // direct links
allM map[*clientTree]struct{} // direct & transitive links
}
// reset clears the cache.
func (lc *linkCache) reset() {
lc.directM = nil
lc.allM = nil
}
// add adds a direct link to the cache.
func (lc *linkCache) add(ct *clientTree) {
if lc.directM == nil {
lc.directM = make(map[*clientTree]struct{})
}
if _, ok := lc.directM[ct]; !ok {
lc.invalidate()
}
lc.directM[ct] = struct{}{}
}
// invalidate resets the cache of transitive links.
func (lc *linkCache) invalidate() {
lc.allM = nil
}
// valid returns true when the cache of transitive links is up-to-date.
func (lc *linkCache) valid() bool {
// Re-check validity of child caches to catch updates.
for ct := range lc.allM {
if ct != lc.self && !ct.linkCache.valid() {
lc.allM = nil
break
}
}
return lc.allM != nil
}
// all returns all trees reachable through the cache.
func (lc *linkCache) all() map[*clientTree]struct{} {
if lc.valid() {
return lc.allM
}
// Remake lc.allM it by taking the union of all() across children.
m := make(map[*clientTree]struct{})
if lc.self != nil {
m[lc.self] = struct{}{}
}
for ct := range lc.directM {
m[ct] = struct{}{}
for lt := range ct.linkCache.all() {
m[lt] = struct{}{}
}
}
lc.allM = m
return m
}

384
p2p/dnsdisc/tree.go Normal file
View File

@ -0,0 +1,384 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dnsdisc
import (
"bytes"
"crypto/ecdsa"
"encoding/base32"
"encoding/base64"
"fmt"
"io"
"sort"
"strings"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/rlp"
"golang.org/x/crypto/sha3"
)
// Tree is a merkle tree of node records.
type Tree struct {
root *rootEntry
entries map[string]entry
}
// Sign signs the tree with the given private key and sets the sequence number.
func (t *Tree) Sign(key *ecdsa.PrivateKey, domain string) (url string, err error) {
root := *t.root
sig, err := crypto.Sign(root.sigHash(), key)
if err != nil {
return "", err
}
root.sig = sig
t.root = &root
link := &linkEntry{domain, &key.PublicKey}
return link.url(), nil
}
// SetSignature verifies the given signature and assigns it as the tree's current
// signature if valid.
func (t *Tree) SetSignature(pubkey *ecdsa.PublicKey, signature string) error {
sig, err := b64format.DecodeString(signature)
if err != nil || len(sig) != crypto.SignatureLength {
return errInvalidSig
}
root := *t.root
root.sig = sig
if !root.verifySignature(pubkey) {
return errInvalidSig
}
t.root = &root
return nil
}
// Seq returns the sequence number of the tree.
func (t *Tree) Seq() uint {
return t.root.seq
}
// Signature returns the signature of the tree.
func (t *Tree) Signature() string {
return b64format.EncodeToString(t.root.sig)
}
// ToTXT returns all DNS TXT records required for the tree.
func (t *Tree) ToTXT(domain string) map[string]string {
records := map[string]string{domain: t.root.String()}
for _, e := range t.entries {
sd := subdomain(e)
if domain != "" {
sd = sd + "." + domain
}
records[sd] = e.String()
}
return records
}
// Links returns all links contained in the tree.
func (t *Tree) Links() []string {
var links []string
for _, e := range t.entries {
if le, ok := e.(*linkEntry); ok {
links = append(links, le.url())
}
}
return links
}
// Nodes returns all nodes contained in the tree.
func (t *Tree) Nodes() []*enode.Node {
var nodes []*enode.Node
for _, e := range t.entries {
if ee, ok := e.(*enrEntry); ok {
nodes = append(nodes, ee.node)
}
}
return nodes
}
const (
hashAbbrev = 16
maxChildren = 300 / (hashAbbrev * (13 / 8))
minHashLength = 12
rootPrefix = "enrtree-root=v1"
)
// MakeTree creates a tree containing the given nodes and links.
func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) {
// Sort records by ID and ensure all nodes have a valid record.
records := make([]*enode.Node, len(nodes))
copy(records, nodes)
sortByID(records)
for _, n := range records {
if len(n.Record().Signature()) == 0 {
return nil, fmt.Errorf("can't add node %v: unsigned node record", n.ID())
}
}
// Create the leaf list.
enrEntries := make([]entry, len(records))
for i, r := range records {
enrEntries[i] = &enrEntry{r}
}
linkEntries := make([]entry, len(links))
for i, l := range links {
le, err := parseURL(l)
if err != nil {
return nil, err
}
linkEntries[i] = le
}
// Create intermediate nodes.
t := &Tree{entries: make(map[string]entry)}
eroot := t.build(enrEntries)
t.entries[subdomain(eroot)] = eroot
lroot := t.build(linkEntries)
t.entries[subdomain(lroot)] = lroot
t.root = &rootEntry{seq: seq, eroot: subdomain(eroot), lroot: subdomain(lroot)}
return t, nil
}
func (t *Tree) build(entries []entry) entry {
if len(entries) == 1 {
return entries[0]
}
if len(entries) <= maxChildren {
hashes := make([]string, len(entries))
for i, e := range entries {
hashes[i] = subdomain(e)
t.entries[hashes[i]] = e
}
return &subtreeEntry{hashes}
}
var subtrees []entry
for len(entries) > 0 {
n := maxChildren
if len(entries) < n {
n = len(entries)
}
sub := t.build(entries[:n])
entries = entries[n:]
subtrees = append(subtrees, sub)
t.entries[subdomain(sub)] = sub
}
return t.build(subtrees)
}
func sortByID(nodes []*enode.Node) []*enode.Node {
sort.Slice(nodes, func(i, j int) bool {
return bytes.Compare(nodes[i].ID().Bytes(), nodes[j].ID().Bytes()) < 0
})
return nodes
}
// Entry Types
type entry interface {
fmt.Stringer
}
type (
rootEntry struct {
eroot string
lroot string
seq uint
sig []byte
}
subtreeEntry struct {
children []string
}
enrEntry struct {
node *enode.Node
}
linkEntry struct {
domain string
pubkey *ecdsa.PublicKey
}
)
// Entry Encoding
var (
b32format = base32.StdEncoding.WithPadding(base32.NoPadding)
b64format = base64.URLEncoding
)
func subdomain(e entry) string {
h := sha3.NewLegacyKeccak256()
io.WriteString(h, e.String())
return b32format.EncodeToString(h.Sum(nil)[:16])
}
func (e *rootEntry) String() string {
return fmt.Sprintf(rootPrefix+" e=%s l=%s seq=%d sig=%s", e.eroot, e.lroot, e.seq, b64format.EncodeToString(e.sig))
}
func (e *rootEntry) sigHash() []byte {
h := sha3.NewLegacyKeccak256()
fmt.Fprintf(h, rootPrefix+" e=%s l=%s seq=%d", e.eroot, e.lroot, e.seq)
return h.Sum(nil)
}
func (e *rootEntry) verifySignature(pubkey *ecdsa.PublicKey) bool {
sig := e.sig[:crypto.RecoveryIDOffset] // remove recovery id
return crypto.VerifySignature(crypto.FromECDSAPub(pubkey), e.sigHash(), sig)
}
func (e *subtreeEntry) String() string {
return "enrtree=" + strings.Join(e.children, ",")
}
func (e *enrEntry) String() string {
enc, _ := rlp.EncodeToBytes(e.node.Record())
return "enr=" + b64format.EncodeToString(enc)
}
func (e *linkEntry) String() string {
return "enrtree-link=" + e.link()
}
func (e *linkEntry) url() string {
return "enrtree://" + e.link()
}
func (e *linkEntry) link() string {
return fmt.Sprintf("%s@%s", b32format.EncodeToString(crypto.CompressPubkey(e.pubkey)), e.domain)
}
// Entry Parsing
func parseEntry(e string, validSchemes enr.IdentityScheme) (entry, error) {
switch {
case strings.HasPrefix(e, "enrtree-link="):
return parseLink(e[13:])
case strings.HasPrefix(e, "enrtree="):
return parseSubtree(e[8:])
case strings.HasPrefix(e, "enr="):
return parseENR(e[4:], validSchemes)
default:
return nil, errUnknownEntry
}
}
func parseRoot(e string) (rootEntry, error) {
var eroot, lroot, sig string
var seq uint
if _, err := fmt.Sscanf(e, rootPrefix+" e=%s l=%s seq=%d sig=%s", &eroot, &lroot, &seq, &sig); err != nil {
return rootEntry{}, entryError{"root", errSyntax}
}
if !isValidHash(eroot) || !isValidHash(lroot) {
return rootEntry{}, entryError{"root", errInvalidChild}
}
sigb, err := b64format.DecodeString(sig)
if err != nil || len(sigb) != crypto.SignatureLength {
return rootEntry{}, entryError{"root", errInvalidSig}
}
return rootEntry{eroot, lroot, seq, sigb}, nil
}
func parseLink(e string) (entry, error) {
pos := strings.IndexByte(e, '@')
if pos == -1 {
return nil, entryError{"link", errNoPubkey}
}
keystring, domain := e[:pos], e[pos+1:]
keybytes, err := b32format.DecodeString(keystring)
if err != nil {
return nil, entryError{"link", errBadPubkey}
}
key, err := crypto.DecompressPubkey(keybytes)
if err != nil {
return nil, entryError{"link", errBadPubkey}
}
return &linkEntry{domain, key}, nil
}
func parseSubtree(e string) (entry, error) {
if e == "" {
return &subtreeEntry{}, nil // empty entry is OK
}
hashes := make([]string, 0, strings.Count(e, ","))
for _, c := range strings.Split(e, ",") {
if !isValidHash(c) {
return nil, entryError{"subtree", errInvalidChild}
}
hashes = append(hashes, c)
}
return &subtreeEntry{hashes}, nil
}
func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) {
enc, err := b64format.DecodeString(e)
if err != nil {
return nil, entryError{"enr", errInvalidENR}
}
var rec enr.Record
if err := rlp.DecodeBytes(enc, &rec); err != nil {
return nil, entryError{"enr", err}
}
n, err := enode.New(validSchemes, &rec)
if err != nil {
return nil, entryError{"enr", err}
}
return &enrEntry{n}, nil
}
func isValidHash(s string) bool {
dlen := b32format.DecodedLen(len(s))
if dlen < minHashLength || dlen > 32 || strings.ContainsAny(s, "\n\r") {
return false
}
buf := make([]byte, 32)
_, err := b32format.Decode(buf, []byte(s))
return err == nil
}
// truncateHash truncates the given base32 hash string to the minimum acceptable length.
func truncateHash(hash string) string {
maxLen := b32format.EncodedLen(minHashLength)
if len(hash) < maxLen {
panic(fmt.Errorf("dnsdisc: hash %q is too short", hash))
}
return hash[:maxLen]
}
// URL encoding
// ParseURL parses an enrtree:// URL and returns its components.
func ParseURL(url string) (domain string, pubkey *ecdsa.PublicKey, err error) {
le, err := parseURL(url)
if err != nil {
return "", nil, err
}
return le.domain, le.pubkey, nil
}
func parseURL(url string) (*linkEntry, error) {
const scheme = "enrtree://"
if !strings.HasPrefix(url, scheme) {
return nil, fmt.Errorf("wrong/missing scheme 'enrtree' in URL")
}
le, err := parseLink(url[len(scheme):])
if err != nil {
return nil, err.(entryError).err
}
return le.(*linkEntry), nil
}

144
p2p/dnsdisc/tree_test.go Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package dnsdisc
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/p2p/enode"
)
func TestParseRoot(t *testing.T) {
tests := []struct {
input string
e rootEntry
err error
}{
{
input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=",
err: entryError{"root", errSyntax},
},
{
input: "enrtree-root=v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw=",
err: entryError{"root", errInvalidSig},
},
{
input: "enrtree-root=v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE=",
e: rootEntry{
eroot: "QFT4PBCRX4XQCV3VUYJ6BTCEPU",
lroot: "JGUFMSAGI7KZYB3P7IZW4S5Y3A",
seq: 3,
sig: hexutil.MustDecode("0xdc5997b95c296bc63b3acb594f1f4f21bd66b7c16b5bb5690ce16fe006860ac6761081e686b69685ee0dc588500e5c393237855d831b263b0f78a947ce62511101"),
},
},
}
for i, test := range tests {
e, err := parseRoot(test.input)
if !reflect.DeepEqual(e, test.e) {
t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
}
if err != test.err {
t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
}
}
}
func TestParseEntry(t *testing.T) {
testkey := testKey(signingKeySeed)
tests := []struct {
input string
e entry
err error
}{
// Subtrees:
{
input: "enrtree=1,2",
err: entryError{"subtree", errInvalidChild},
},
{
input: "enrtree=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
err: entryError{"subtree", errInvalidChild},
},
{
input: "enrtree=",
e: &subtreeEntry{},
},
{
input: "enrtree=AAAAAAAAAAAAAAAAAAAA",
e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}},
},
{
input: "enrtree=AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB",
e: &subtreeEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}},
},
// Links
{
input: "enrtree-link=AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org",
e: &linkEntry{"nodes.example.org", &testkey.PublicKey},
},
{
input: "enrtree-link=nodes.example.org",
err: entryError{"link", errNoPubkey},
},
{
input: "enrtree-link=AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org",
err: entryError{"link", errBadPubkey},
},
{
input: "enrtree-link=AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org",
err: entryError{"link", errBadPubkey},
},
// ENRs
{
input: "enr=-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM=",
e: &enrEntry{node: testNode(nodesSeed1)},
},
{
input: "enr=-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=",
err: entryError{"enr", errInvalidENR},
},
// Invalid:
{input: "", err: errUnknownEntry},
{input: "foo", err: errUnknownEntry},
{input: "enrtree", err: errUnknownEntry},
{input: "enrtree-x=", err: errUnknownEntry},
}
for i, test := range tests {
e, err := parseEntry(test.input, enode.ValidSchemes)
if !reflect.DeepEqual(e, test.e) {
t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
}
if err != test.err {
t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
}
}
}
func TestMakeTree(t *testing.T) {
nodes := testNodes(nodesSeed2, 50)
tree, err := MakeTree(2, nodes, nil)
if err != nil {
t.Fatal(err)
}
txt := tree.ToTXT("")
if len(txt) < len(nodes)+1 {
t.Fatal("too few TXT records in output")
}
}

View File

@ -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

26
vendor/github.com/cloudflare/cloudflare-go/LICENSE generated vendored Normal file
View File

@ -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.

107
vendor/github.com/cloudflare/cloudflare-go/README.md generated vendored Normal file
View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

114
vendor/github.com/cloudflare/cloudflare-go/accounts.go generated vendored Normal file
View File

@ -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
}

120
vendor/github.com/cloudflare/cloudflare-go/argo.go generated vendored Normal file
View File

@ -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
}

143
vendor/github.com/cloudflare/cloudflare-go/auditlogs.go generated vendored Normal file
View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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")
}

View File

@ -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
}

174
vendor/github.com/cloudflare/cloudflare-go/dns.go generated vendored Normal file
View File

@ -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
}

40
vendor/github.com/cloudflare/cloudflare-go/duration.go generated vendored Normal file
View File

@ -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))
)

50
vendor/github.com/cloudflare/cloudflare-go/errors.go generated vendored Normal file
View File

@ -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()
}

241
vendor/github.com/cloudflare/cloudflare-go/filter.go generated vendored Normal file
View File

@ -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
}

280
vendor/github.com/cloudflare/cloudflare-go/firewall.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

13
vendor/github.com/cloudflare/cloudflare-go/go.mod generated vendored Normal file
View File

@ -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
)

26
vendor/github.com/cloudflare/cloudflare-go/go.sum generated vendored Normal file
View File

@ -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=

44
vendor/github.com/cloudflare/cloudflare-go/ips.go generated vendored Normal file
View File

@ -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
}

52
vendor/github.com/cloudflare/cloudflare-go/keyless.go generated vendored Normal file
View File

@ -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() {
}

View File

@ -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
}

151
vendor/github.com/cloudflare/cloudflare-go/lockdown.go generated vendored Normal file
View File

@ -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
}

224
vendor/github.com/cloudflare/cloudflare-go/logpush.go generated vendored Normal file
View File

@ -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
}

101
vendor/github.com/cloudflare/cloudflare-go/options.go generated vendored Normal file
View File

@ -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
}

169
vendor/github.com/cloudflare/cloudflare-go/origin_ca.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

297
vendor/github.com/cloudflare/cloudflare-go/railgun.go generated vendored Normal file
View File

@ -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)
}

View File

@ -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
}

175
vendor/github.com/cloudflare/cloudflare-go/registrar.go generated vendored Normal file
View File

@ -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
}

View File

@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

158
vendor/github.com/cloudflare/cloudflare-go/spectrum.go generated vendored Normal file
View File

@ -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
}

157
vendor/github.com/cloudflare/cloudflare-go/ssl.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

113
vendor/github.com/cloudflare/cloudflare-go/user.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

300
vendor/github.com/cloudflare/cloudflare-go/waf.go generated vendored Normal file
View File

@ -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
}

314
vendor/github.com/cloudflare/cloudflare-go/workers.go generated vendored Normal file
View File

@ -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
}

View File

@ -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
}

740
vendor/github.com/cloudflare/cloudflare-go/zone.go generated vendored Normal file
View File

@ -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
}

27
vendor/golang.org/x/time/LICENSE generated vendored Normal file
View File

@ -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.

22
vendor/golang.org/x/time/PATENTS generated vendored Normal file
View File

@ -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.

374
vendor/golang.org/x/time/rate/rate.go generated vendored Normal file
View File

@ -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)
}

12
vendor/vendor.json vendored
View File

@ -56,6 +56,12 @@
"revision": "165db2f241fd235aec29ba6d9b1ccd5f1c14637c", "revision": "165db2f241fd235aec29ba6d9b1ccd5f1c14637c",
"revisionTime": "2015-01-22T07:26:53Z" "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=", "checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",
"path": "github.com/davecgh/go-spew/spew", "path": "github.com/davecgh/go-spew/spew",
@ -844,6 +850,12 @@
"revision": "31e7599a6c37728c25ca34167be099d072ad335d", "revision": "31e7599a6c37728c25ca34167be099d072ad335d",
"revisionTime": "2019-04-05T05:38:27Z" "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=", "checksumSHA1": "CEFTYXtWmgSh+3Ik1NmDaJcz4E0=",
"path": "gopkg.in/check.v1", "path": "gopkg.in/check.v1",