diff --git a/cmd/devp2p/dns_route53.go b/cmd/devp2p/dns_route53.go
new file mode 100644
index 0000000000..1e9b39b0ea
--- /dev/null
+++ b/cmd/devp2p/dns_route53.go
@@ -0,0 +1,260 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see .
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/route53"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/p2p/dnsdisc"
+ "gopkg.in/urfave/cli.v1"
+)
+
+var (
+ route53AccessKeyFlag = cli.StringFlag{
+ Name: "access-key-id",
+ Usage: "AWS Access Key ID",
+ EnvVar: "AWS_ACCESS_KEY_ID",
+ }
+ route53AccessSecretFlag = cli.StringFlag{
+ Name: "access-key-secret",
+ Usage: "AWS Access Key Secret",
+ EnvVar: "AWS_SECRET_ACCESS_KEY",
+ }
+ route53ZoneIDFlag = cli.StringFlag{
+ Name: "zone-id",
+ Usage: "Route53 Zone ID",
+ }
+)
+
+type route53Client struct {
+ api *route53.Route53
+ zoneID string
+}
+
+// newRoute53Client sets up a Route53 API client from command line flags.
+func newRoute53Client(ctx *cli.Context) *route53Client {
+ akey := ctx.String(route53AccessKeyFlag.Name)
+ asec := ctx.String(route53AccessSecretFlag.Name)
+ if akey == "" || asec == "" {
+ exit(fmt.Errorf("need Route53 Access Key ID and secret proceed"))
+ }
+ config := &aws.Config{Credentials: credentials.NewStaticCredentials(akey, asec, "")}
+ session, err := session.NewSession(config)
+ if err != nil {
+ exit(fmt.Errorf("can't create AWS session: %v", err))
+ }
+ return &route53Client{
+ api: route53.New(session),
+ zoneID: ctx.String(route53ZoneIDFlag.Name),
+ }
+}
+
+// deploy uploads the given tree to Route53.
+func (c *route53Client) deploy(name string, t *dnsdisc.Tree) error {
+ if err := c.checkZone(name); err != nil {
+ return err
+ }
+
+ // Compute DNS changes.
+ records := t.ToTXT(name)
+ changes, err := c.computeChanges(name, records)
+ if err != nil {
+ return err
+ }
+ if len(changes) == 0 {
+ log.Info("No DNS changes needed")
+ return nil
+ }
+
+ // Submit change request.
+ log.Info(fmt.Sprintf("Submitting %d changes to Route53", len(changes)))
+ batch := new(route53.ChangeBatch)
+ batch.SetChanges(changes)
+ batch.SetComment(fmt.Sprintf("enrtree update of %s at seq %d", name, t.Seq()))
+ req := &route53.ChangeResourceRecordSetsInput{HostedZoneId: &c.zoneID, ChangeBatch: batch}
+ resp, err := c.api.ChangeResourceRecordSets(req)
+ if err != nil {
+ return err
+ }
+
+ // Wait for the change to be applied.
+ log.Info(fmt.Sprintf("Waiting for change request %s", *resp.ChangeInfo.Id))
+ wreq := &route53.GetChangeInput{Id: resp.ChangeInfo.Id}
+ return c.api.WaitUntilResourceRecordSetsChanged(wreq)
+}
+
+// checkZone verifies zone information for the given domain.
+func (c *route53Client) checkZone(name string) (err error) {
+ if c.zoneID == "" {
+ c.zoneID, err = c.findZoneID(name)
+ }
+ return err
+}
+
+// findZoneID searches for the Zone ID containing the given domain.
+func (c *route53Client) findZoneID(name string) (string, error) {
+ log.Info(fmt.Sprintf("Finding Route53 Zone ID for %s", name))
+ var req route53.ListHostedZonesByNameInput
+ for {
+ resp, err := c.api.ListHostedZonesByName(&req)
+ if err != nil {
+ return "", err
+ }
+ for _, zone := range resp.HostedZones {
+ if isSubdomain(name, *zone.Name) {
+ return *zone.Id, nil
+ }
+ }
+ if !*resp.IsTruncated {
+ break
+ }
+ req.DNSName = resp.NextDNSName
+ req.HostedZoneId = resp.NextHostedZoneId
+ }
+ return "", errors.New("can't find zone ID for " + name)
+}
+
+// computeChanges creates DNS changes for the given record.
+func (c *route53Client) computeChanges(name string, records map[string]string) ([]*route53.Change, 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
+
+ // Get existing records.
+ existing, err := c.collectRecords(name)
+ if err != nil {
+ return nil, err
+ }
+ log.Info(fmt.Sprintf("Found %d TXT records", len(existing)))
+
+ var changes []*route53.Change
+ for path, val := range records {
+ ttl := 1
+ if path != name {
+ ttl = 2147483647
+ }
+
+ prevRecords, exists := existing[path]
+ prevValue := combineTXT(prevRecords)
+ if !exists {
+ // Entry is unknown, push a new one
+ log.Info(fmt.Sprintf("Creating %s = %q", path, val))
+ changes = append(changes, newTXTChange("CREATE", path, ttl, splitTXT(val)))
+ } else if prevValue != val {
+ // Entry already exists, only change its content.
+ log.Info(fmt.Sprintf("Updating %s from %q to %q", path, prevValue, val))
+ changes = append(changes, newTXTChange("UPSERT", path, ttl, splitTXT(val)))
+ } else {
+ log.Info(fmt.Sprintf("Skipping %s = %q", path, val))
+ }
+ }
+
+ // Iterate over the old records and delete anything stale.
+ for path, values := range existing {
+ if _, ok := records[path]; ok {
+ continue
+ }
+ // Stale entry, nuke it.
+ log.Info(fmt.Sprintf("Deleting %s = %q", path, combineTXT(values)))
+ changes = append(changes, newTXTChange("DELETE", path, 1, values))
+ }
+ return changes, nil
+}
+
+// collectRecords collects all TXT records below the given name.
+func (c *route53Client) collectRecords(name string) (map[string][]string, error) {
+ log.Info(fmt.Sprintf("Retrieving existing TXT records on %s (%s)", name, c.zoneID))
+ var req route53.ListResourceRecordSetsInput
+ req.SetHostedZoneId(c.zoneID)
+ existing := make(map[string][]string)
+ err := c.api.ListResourceRecordSetsPages(&req, func(resp *route53.ListResourceRecordSetsOutput, last bool) bool {
+ for _, set := range resp.ResourceRecordSets {
+ if !isSubdomain(*set.Name, name) || *set.Type != "TXT" {
+ continue
+ }
+ name := strings.TrimSuffix(*set.Name, ".")
+ for _, rec := range set.ResourceRecords {
+ existing[name] = append(existing[name], *rec.Value)
+ }
+ }
+ return true
+ })
+ return existing, err
+}
+
+// newTXTChange creates a change to a TXT record.
+func newTXTChange(action, name string, ttl int, values []string) *route53.Change {
+ var c route53.Change
+ var r route53.ResourceRecordSet
+ var rrs []*route53.ResourceRecord
+ for _, val := range values {
+ rr := new(route53.ResourceRecord)
+ rr.SetValue(val)
+ rrs = append(rrs, rr)
+ }
+ r.SetType("TXT")
+ r.SetName(name)
+ r.SetTTL(int64(ttl))
+ r.SetResourceRecords(rrs)
+ c.SetAction(action)
+ c.SetResourceRecordSet(&r)
+ return &c
+}
+
+// isSubdomain returns true if name is a subdomain of domain.
+func isSubdomain(name, domain string) bool {
+ domain = strings.TrimSuffix(domain, ".")
+ name = strings.TrimSuffix(name, ".")
+ return strings.HasSuffix("."+name, "."+domain)
+}
+
+// combineTXT concatenates the given quoted strings into a single unquoted string.
+func combineTXT(values []string) string {
+ result := ""
+ for _, v := range values {
+ if v[0] == '"' {
+ v = v[1 : len(v)-1]
+ }
+ result += v
+ }
+ return result
+}
+
+// splitTXT splits value into a list of quoted 255-character strings.
+func splitTXT(value string) []string {
+ var result []string
+ for len(value) > 0 {
+ rlen := len(value)
+ if rlen > 253 {
+ rlen = 253
+ }
+ result = append(result, strconv.Quote(value[:rlen]))
+ value = value[rlen:]
+ }
+ return result
+}
diff --git a/cmd/devp2p/dnscmd.go b/cmd/devp2p/dnscmd.go
index f245104053..287d6e6c76 100644
--- a/cmd/devp2p/dnscmd.go
+++ b/cmd/devp2p/dnscmd.go
@@ -42,6 +42,7 @@ var (
dnsSignCommand,
dnsTXTCommand,
dnsCloudflareCommand,
+ dnsRoute53Command,
},
}
dnsSyncCommand = cli.Command{
@@ -66,11 +67,18 @@ var (
}
dnsCloudflareCommand = cli.Command{
Name: "to-cloudflare",
- Usage: "Deploy DNS TXT records to cloudflare",
+ Usage: "Deploy DNS TXT records to CloudFlare",
ArgsUsage: "",
Action: dnsToCloudflare,
Flags: []cli.Flag{cloudflareTokenFlag, cloudflareZoneIDFlag},
}
+ dnsRoute53Command = cli.Command{
+ Name: "to-route53",
+ Usage: "Deploy DNS TXT records to Amazon Route53",
+ ArgsUsage: "",
+ Action: dnsToRoute53,
+ Flags: []cli.Flag{route53AccessKeyFlag, route53AccessSecretFlag, route53ZoneIDFlag},
+ }
)
var (
@@ -194,6 +202,19 @@ func dnsToCloudflare(ctx *cli.Context) error {
return client.deploy(domain, t)
}
+// dnsToRoute53 peforms dnsRoute53Command.
+func dnsToRoute53(ctx *cli.Context) error {
+ if ctx.NArg() < 1 {
+ return fmt.Errorf("need tree definition directory as argument")
+ }
+ domain, t, err := loadTreeDefinitionForExport(ctx.Args().Get(0))
+ if err != nil {
+ return err
+ }
+ client := newRoute53Client(ctx)
+ return client.deploy(domain, t)
+}
+
// loadSigningKey loads a private key in Ethereum keystore format.
func loadSigningKey(keyfile string) *ecdsa.PrivateKey {
keyjson, err := ioutil.ReadFile(keyfile)
diff --git a/go.mod b/go.mod
index a280949e94..223086f8c5 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/VictoriaMetrics/fastcache v1.5.3
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847
+ github.com/aws/aws-sdk-go v1.25.48
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6
github.com/cespare/cp v0.1.0
github.com/cespare/xxhash/v2 v2.1.1 // indirect
diff --git a/go.sum b/go.sum
index 515207bca3..edbb5ea2e0 100644
--- a/go.sum
+++ b/go.sum
@@ -33,6 +33,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847 h1:rtI0fD4oG/8eVokGVPYJEW1F88p1ZNgXiEIs9thEE4A=
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
+github.com/aws/aws-sdk-go v1.25.48 h1:J82DYDGZHOKHdhx6hD24Tm30c2C3GchYGfN0mf9iKUk=
+github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6 h1:Eey/GGQ/E5Xp1P2Lyx1qj007hLZfbi0+CoVeJruGCtI=
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
@@ -99,6 +101,7 @@ github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883 h1:FSeK4fZCo
github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 h1:6OvNmYgJyexcZ3pYbTI9jWx5tHo1Dee/tWbLMfPe2TA=
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21 h1:F/iKcka0K2LgnKy/fgSBf235AETtm1n1TvBzqu40LE0=
github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356 h1:I/yrLt2WilKxlQKCM52clh5rGzTKpVctGT1lH4Dc8Jw=