mirror of https://github.com/ethereum/go-ethereum
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.pull/19352/head^2
parent
32b07e8b1f
commit
0568e81701
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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} |
||||
} |
@ -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 |
||||
} |
@ -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 |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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") |
||||
} |
||||
} |
@ -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 |
||||
|
@ -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. |
@ -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. |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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)) |
||||
} |
||||
} |
@ -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") |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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)) |
||||
) |
@ -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() |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
) |
@ -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= |
@ -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 |
||||
} |
@ -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() { |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
|
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -0,0 +1,5 @@ |
||||
{ |
||||
"extends": [ |
||||
"config:base" |
||||
] |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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. |
@ -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. |
@ -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) |
||||
} |
Loading…
Reference in new issue