Unverified Commit 0568e817 authored by Felix Lange's avatar Felix Lange Committed by GitHub

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.
parent 32b07e8b
...@@ -19,10 +19,10 @@ package main ...@@ -19,10 +19,10 @@ package main
import ( import (
"fmt" "fmt"
"net" "net"
"sort"
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
...@@ -38,23 +38,34 @@ var ( ...@@ -38,23 +38,34 @@ var (
discv4PingCommand, discv4PingCommand,
discv4RequestRecordCommand, discv4RequestRecordCommand,
discv4ResolveCommand, discv4ResolveCommand,
discv4ResolveJSONCommand,
}, },
} }
discv4PingCommand = cli.Command{ discv4PingCommand = cli.Command{
Name: "ping", Name: "ping",
Usage: "Sends ping to a node", Usage: "Sends ping to a node",
Action: discv4Ping, Action: discv4Ping,
ArgsUsage: "<node>",
} }
discv4RequestRecordCommand = cli.Command{ discv4RequestRecordCommand = cli.Command{
Name: "requestenr", Name: "requestenr",
Usage: "Requests a node record using EIP-868 enrRequest", Usage: "Requests a node record using EIP-868 enrRequest",
Action: discv4RequestRecord, Action: discv4RequestRecord,
ArgsUsage: "<node>",
} }
discv4ResolveCommand = cli.Command{ discv4ResolveCommand = cli.Command{
Name: "resolve", Name: "resolve",
Usage: "Finds a node in the DHT", Usage: "Finds a node in the DHT",
Action: discv4Resolve, Action: discv4Resolve,
Flags: []cli.Flag{bootnodesFlag}, ArgsUsage: "<node>",
Flags: []cli.Flag{bootnodesFlag},
}
discv4ResolveJSONCommand = cli.Command{
Name: "resolve-json",
Usage: "Re-resolves nodes in a nodes.json file",
Action: discv4ResolveJSON,
Flags: []cli.Flag{bootnodesFlag},
ArgsUsage: "<nodes.json file>",
} }
) )
...@@ -64,10 +75,8 @@ var bootnodesFlag = cli.StringFlag{ ...@@ -64,10 +75,8 @@ var bootnodesFlag = cli.StringFlag{
} }
func discv4Ping(ctx *cli.Context) error { func discv4Ping(ctx *cli.Context) error {
n, disc, err := getNodeArgAndStartV4(ctx) n := getNodeArg(ctx)
if err != nil { disc := startV4(ctx)
return err
}
defer disc.Close() defer disc.Close()
start := time.Now() start := time.Now()
...@@ -79,10 +88,8 @@ func discv4Ping(ctx *cli.Context) error { ...@@ -79,10 +88,8 @@ func discv4Ping(ctx *cli.Context) error {
} }
func discv4RequestRecord(ctx *cli.Context) error { func discv4RequestRecord(ctx *cli.Context) error {
n, disc, err := getNodeArgAndStartV4(ctx) n := getNodeArg(ctx)
if err != nil { disc := startV4(ctx)
return err
}
defer disc.Close() defer disc.Close()
respN, err := disc.RequestENR(n) respN, err := disc.RequestENR(n)
...@@ -94,33 +101,43 @@ func discv4RequestRecord(ctx *cli.Context) error { ...@@ -94,33 +101,43 @@ func discv4RequestRecord(ctx *cli.Context) error {
} }
func discv4Resolve(ctx *cli.Context) error { func discv4Resolve(ctx *cli.Context) error {
n, disc, err := getNodeArgAndStartV4(ctx) n := getNodeArg(ctx)
if err != nil { disc := startV4(ctx)
return err
}
defer disc.Close() defer disc.Close()
fmt.Println(disc.Resolve(n).String()) fmt.Println(disc.Resolve(n).String())
return nil return nil
} }
func getNodeArgAndStartV4(ctx *cli.Context) (*enode.Node, *discover.UDPv4, error) { func discv4ResolveJSON(ctx *cli.Context) error {
if ctx.NArg() != 1 { if ctx.NArg() < 1 {
return nil, nil, fmt.Errorf("missing node as command-line argument") return fmt.Errorf("need nodes file as argument")
} }
n, err := parseNode(ctx.Args()[0]) disc := startV4(ctx)
if err != nil { defer disc.Close()
return nil, nil, err file := ctx.Args().Get(0)
// Load existing nodes in file.
var nodes []*enode.Node
if common.FileExist(file) {
nodes = loadNodesJSON(file).nodes()
} }
var bootnodes []*enode.Node // Add nodes from command line arguments.
if commandHasFlag(ctx, bootnodesFlag) { for i := 1; i < ctx.NArg(); i++ {
bootnodes, err = parseBootnodes(ctx) n, err := parseNode(ctx.Args().Get(i))
if err != nil { if err != nil {
return nil, nil, err exit(err)
} }
nodes = append(nodes, n)
}
result := make(nodeSet, len(nodes))
for _, n := range nodes {
n = disc.Resolve(n)
result[n.ID()] = nodeJSON{Seq: n.Seq(), N: n}
} }
disc, err := startV4(bootnodes) writeNodesJSON(file, result)
return n, disc, err return nil
} }
func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) { func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
...@@ -139,28 +156,39 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) { ...@@ -139,28 +156,39 @@ func parseBootnodes(ctx *cli.Context) ([]*enode.Node, error) {
return nodes, nil return nodes, nil
} }
// commandHasFlag returns true if the current command supports the given flag. // startV4 starts an ephemeral discovery V4 node.
func commandHasFlag(ctx *cli.Context, flag cli.Flag) bool { func startV4(ctx *cli.Context) *discover.UDPv4 {
flags := ctx.FlagNames() socket, ln, cfg, err := listen()
sort.Strings(flags) if err != nil {
i := sort.SearchStrings(flags, flag.GetName()) exit(err)
return i != len(flags) && flags[i] == flag.GetName() }
if commandHasFlag(ctx, bootnodesFlag) {
bn, err := parseBootnodes(ctx)
if err != nil {
exit(err)
}
cfg.Bootnodes = bn
}
disc, err := discover.ListenV4(socket, ln, cfg)
if err != nil {
exit(err)
}
return disc
} }
// startV4 starts an ephemeral discovery V4 node. func listen() (*net.UDPConn, *enode.LocalNode, discover.Config, error) {
func startV4(bootnodes []*enode.Node) (*discover.UDPv4, error) {
var cfg discover.Config var cfg discover.Config
cfg.Bootnodes = bootnodes
cfg.PrivateKey, _ = crypto.GenerateKey() cfg.PrivateKey, _ = crypto.GenerateKey()
db, _ := enode.OpenDB("") db, _ := enode.OpenDB("")
ln := enode.NewLocalNode(db, cfg.PrivateKey) ln := enode.NewLocalNode(db, cfg.PrivateKey)
socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{0, 0, 0, 0}}) socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{0, 0, 0, 0}})
if err != nil { if err != nil {
return nil, err db.Close()
return nil, nil, cfg, err
} }
addr := socket.LocalAddr().(*net.UDPAddr) addr := socket.LocalAddr().(*net.UDPAddr)
ln.SetFallbackIP(net.IP{127, 0, 0, 1}) ln.SetFallbackIP(net.IP{127, 0, 0, 1})
ln.SetFallbackUDP(addr.Port) ln.SetFallbackUDP(addr.Port)
return discover.ListenUDP(socket, ln, cfg) return socket, ln, cfg, nil
} }
// 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
}
This diff is collapsed.
...@@ -20,8 +20,10 @@ import ( ...@@ -20,8 +20,10 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
) )
...@@ -57,12 +59,38 @@ func init() { ...@@ -57,12 +59,38 @@ func init() {
app.Commands = []cli.Command{ app.Commands = []cli.Command{
enrdumpCommand, enrdumpCommand,
discv4Command, discv4Command,
dnsCommand,
} }
} }
func main() { func main() {
if err := app.Run(os.Args); err != nil { exit(app.Run(os.Args))
fmt.Fprintln(os.Stderr, err) }
os.Exit(1)
// commandHasFlag returns true if the current command supports the given flag.
func commandHasFlag(ctx *cli.Context, flag cli.Flag) bool {
flags := ctx.FlagNames()
sort.Strings(flags)
i := sort.SearchStrings(flags, flag.GetName())
return i != len(flags) && flags[i] == flag.GetName()
}
// getNodeArg handles the common case of a single node descriptor argument.
func getNodeArg(ctx *cli.Context) *enode.Node {
if ctx.NArg() != 1 {
exit("missing node as command-line argument")
}
n, err := parseNode(ctx.Args()[0])
if err != nil {
exit(err)
}
return n
}
func exit(err interface{}) {
if err == nil {
os.Exit(0)
} }
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
} }
// 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
}
// 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}
}
// 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
}
// 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
// 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)
}
// 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
}
This diff is collapsed.
// 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")
}
}
# 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
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.
# 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.
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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)
}
This diff is collapsed.
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")
}
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
}
This diff is collapsed.
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))
)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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
)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment