diff --git a/cmd/algons/dnsCmd.go b/cmd/algons/dnsCmd.go index 8083c60e1b..c9023faf96 100644 --- a/cmd/algons/dnsCmd.go +++ b/cmd/algons/dnsCmd.go @@ -20,6 +20,7 @@ import ( "bufio" "context" "fmt" + "io/ioutil" "net" "os" "regexp" @@ -40,6 +41,8 @@ var ( recordType string noPrompt bool excludePattern string + exportNetwork string + outputFilename string ) func init() { @@ -47,6 +50,10 @@ func init() { dnsCmd.AddCommand(addCmd) dnsCmd.AddCommand(deleteCmd) dnsCmd.AddCommand(listCmd) + dnsCmd.AddCommand(exportCmd) + + listCmd.AddCommand(listRecordsCmd) + listCmd.AddCommand(listZonesCmd) addCmd.Flags().StringVarP(&addFromName, "from", "f", "", "From name to add new DNS entry") addCmd.MarkFlagRequired("from") @@ -58,9 +65,13 @@ func init() { deleteCmd.Flags().BoolVarP(&noPrompt, "no-prompt", "y", false, "No prompting for records deletion") deleteCmd.Flags().StringVarP(&excludePattern, "exclude", "e", "", "name records exclude pattern") - listCmd.Flags().StringVarP(&listNetwork, "network", "n", "", "Domain name for records to list") - listCmd.Flags().StringVarP(&recordType, "recordType", "t", "", "DNS record type to list (A, CNAME, SRV)") - listCmd.MarkFlagRequired("network") + listRecordsCmd.Flags().StringVarP(&listNetwork, "network", "n", "", "Domain name for records to list") + listRecordsCmd.Flags().StringVarP(&recordType, "recordType", "t", "", "DNS record type to list (A, CNAME, SRV)") + listRecordsCmd.MarkFlagRequired("network") + + exportCmd.Flags().StringVarP(&exportNetwork, "network", "n", "", "Domain name to export") + exportCmd.MarkFlagRequired("network") + exportCmd.Flags().StringVarP(&outputFilename, "zonefile", "z", "", "Output file for backup ( intead of outputing it to stdout ) ") } type byIP []net.IP @@ -81,8 +92,17 @@ var dnsCmd = &cobra.Command{ var listCmd = &cobra.Command{ Use: "list", - Short: "List the DNS/SRV entries of the given network", - Long: "List the DNS/SRV entries of the given network", + Short: "List the A/SRV/Zones entries of the given network", + Long: "List the A/SRV/Zones entries of the given network", + Run: func(cmd *cobra.Command, args []string) { + cmd.HelpFunc()(cmd, args) + }, +} + +var listRecordsCmd = &cobra.Command{ + Use: "records", + Short: "List the A/SRV entries of the given network", + Long: "List the A/SRV entries of the given network", Run: func(cmd *cobra.Command, args []string) { recordType = strings.ToUpper(recordType) if recordType == "" || recordType == "A" || recordType == "CNAME" || recordType == "SRV" { @@ -94,6 +114,17 @@ var listCmd = &cobra.Command{ }, } +var listZonesCmd = &cobra.Command{ + Use: "zones", + Short: "List the zones", + Long: "List the zones", + Run: func(cmd *cobra.Command, args []string) { + if !doListZones() { + os.Exit(1) + } + }, +} + var checkCmd = &cobra.Command{ Use: "check", Short: "Check the status", @@ -140,6 +171,16 @@ var deleteCmd = &cobra.Command{ }, } +var exportCmd = &cobra.Command{ + Use: "export", + Short: "Export DNS record entries for a specified network", + Run: func(cmd *cobra.Command, args []string) { + if !doExportZone(exportNetwork, outputFilename) { + os.Exit(1) + } + }, +} + func doAddDNS(from string, to string) (err error) { cfZoneID, cfEmail, cfKey, err := getClouldflareCredentials() if err != nil { @@ -166,11 +207,23 @@ func doAddDNS(from string, to string) (err error) { return } -func getClouldflareCredentials() (zoneID string, email string, authKey string, err error) { - zoneID = os.Getenv("CLOUDFLARE_ZONE_ID") +func getClouldflareAuthCredentials() (email string, authKey string, err error) { email = os.Getenv("CLOUDFLARE_EMAIL") authKey = os.Getenv("CLOUDFLARE_AUTH_KEY") - if zoneID == "" || email == "" || authKey == "" { + if email == "" || authKey == "" { + err = fmt.Errorf("one or more credentials missing from ENV") + } + return +} + +func getClouldflareCredentials() (zoneID string, email string, authKey string, err error) { + email, authKey, err = getClouldflareAuthCredentials() + if err != nil { + return + } + + zoneID = os.Getenv("CLOUDFLARE_ZONE_ID") + if zoneID == "" { err = fmt.Errorf("one or more credentials missing from ENV") } return @@ -333,3 +386,64 @@ func listEntries(listNetwork string, recordType string) { } } } + +func doExportZone(network string, outputFilename string) bool { + cfEmail, cfKey, err := getClouldflareAuthCredentials() + if err != nil { + fmt.Fprintf(os.Stderr, "error getting DNS credentials: %v", err) + return false + } + cloudflareCred := cloudflare.NewCred(cfEmail, cfKey) + zones, err := cloudflareCred.GetZones(context.Background()) + if err != nil { + fmt.Fprintf(os.Stderr, "Error retrieving zones entries: %v\n", err) + return false + } + zoneID := "" + // find a zone that matches the requested network name. + for _, z := range zones { + if z.DomainName == network { + zoneID = z.ZoneID + break + } + fmt.Printf("%s : %s\n", z.DomainName, z.ZoneID) + } + if zoneID == "" { + fmt.Fprintf(os.Stderr, "No matching zoneID was found for %s\n", network) + return false + } + cloudflareDNS := cloudflare.NewDNS(zoneID, cfEmail, cfKey) + exportedZone, err := cloudflareDNS.ExportZone(context.Background()) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to export zone : %v\n", err) + return false + } + if outputFilename != "" { + err = ioutil.WriteFile(outputFilename, exportedZone, 0666) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to write exported zone file : %v\n", err) + return false + } + } else { + fmt.Fprint(os.Stdout, string(exportedZone)) + } + return true +} + +func doListZones() bool { + cfEmail, cfKey, err := getClouldflareAuthCredentials() + if err != nil { + fmt.Fprintf(os.Stderr, "error getting DNS credentials: %v", err) + return false + } + cloudflareCred := cloudflare.NewCred(cfEmail, cfKey) + zones, err := cloudflareCred.GetZones(context.Background()) + if err != nil { + fmt.Fprintf(os.Stderr, "Error listing zones entries: %v\n", err) + return false + } + for _, z := range zones { + fmt.Printf("%s : %s\n", z.DomainName, z.ZoneID) + } + return true +} diff --git a/tools/network/cloudflare/cloudflare.go b/tools/network/cloudflare/cloudflare.go index f62193706c..a4be998911 100644 --- a/tools/network/cloudflare/cloudflare.go +++ b/tools/network/cloudflare/cloudflare.go @@ -19,6 +19,7 @@ package cloudflare import ( "context" "fmt" + "io/ioutil" "net/http" "strings" ) @@ -29,19 +30,34 @@ const ( AutomaticTTL = 1 ) -// DNS is the cloudflare package main access class. Initiate an instance of this class to access the clouldflare APIs. -type DNS struct { - zoneID string +// Cred contains the credentials used to authenticate with the cloudflare API. +type Cred struct { authEmail string authKey string } +// DNS is the cloudflare package main access class. Initiate an instance of this class to access the clouldflare APIs. +type DNS struct { + zoneID string + Cred +} + +// NewCred creates a new credential structure used to authenticate with the cloudflare service. +func NewCred(authEmail string, authKey string) *Cred { + return &Cred{ + authEmail: authEmail, + authKey: authKey, + } +} + // NewDNS create a new instance of clouldflare DNS services class func NewDNS(zoneID string, authEmail string, authKey string) *DNS { return &DNS{ - zoneID: zoneID, - authEmail: authEmail, - authKey: authKey, + zoneID: zoneID, + Cred: Cred{ + authEmail: authEmail, + authKey: authKey, + }, } } @@ -241,3 +257,59 @@ func (d *DNS) UpdateSRVRecord(ctx context.Context, recordID string, name string, } return nil } + +// Zone represent a single zone on the cloudflare API. +type Zone struct { + DomainName string + ZoneID string +} + +// GetZones returns a list of zones that are associated with cloudflare. +func (c *Cred) GetZones(ctx context.Context) (zones []Zone, err error) { + request, err := getZonesRequest(c.authEmail, c.authKey) + if err != nil { + return nil, err + } + client := &http.Client{} + response, err := client.Do(request.WithContext(ctx)) + if err != nil { + return nil, err + } + + parsedResponse, err := parseGetZonesResponse(response) + if err != nil { + return nil, err + } + if parsedResponse.Success == false { + return nil, fmt.Errorf("failed to retrieve zone records : %v", parsedResponse) + } + + for _, z := range parsedResponse.Result { + zones = append(zones, + Zone{ + DomainName: z.Name, + ZoneID: z.ID, + }, + ) + } + return zones, err +} + +// ExportZone exports the zone into a BIND config bytes array +func (d *DNS) ExportZone(ctx context.Context) (exportedZoneBytes []byte, err error) { + request, err := exportZoneRequest(d.zoneID, d.authEmail, d.authKey) + if err != nil { + return nil, err + } + client := &http.Client{} + response, err := client.Do(request.WithContext(ctx)) + if err != nil { + return nil, err + } + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/tools/network/cloudflare/zones.go b/tools/network/cloudflare/zones.go new file mode 100644 index 0000000000..944e0ed546 --- /dev/null +++ b/tools/network/cloudflare/zones.go @@ -0,0 +1,97 @@ +// Copyright (C) 2019 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see . + +package cloudflare + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" +) + +func getZonesRequest(authEmail, authKey string) (*http.Request, error) { + // construct the query + requestURI, err := url.Parse(cloudFlareURI) + if err != nil { + return nil, err + } + requestURI.Path = requestURI.Path + "zones" + request, err := http.NewRequest("GET", requestURI.String(), nil) + if err != nil { + return nil, err + } + addHeaders(request, authEmail, authKey) + return request, nil +} + +// GetZonesResult is the JSON response for a DNS create request +type GetZonesResult struct { + Success bool `json:"success"` + Errors []interface{} `json:"errors"` + Messages []interface{} `json:"messages"` + Result []GetZonesResultItem `json:"result"` + ResultInfo GetZonesResultPage `json:"result_info"` +} + +// GetZonesResultPage is the result of the response for the DNS create request +type GetZonesResultPage struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + TotalPages int `json:"total_pages"` + Count int `json:"count"` + TotalCount int `json:"total_count"` +} + +// GetZonesResultItem is the result of the response for the DNS create request +type GetZonesResultItem struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Paused bool `json:"paused"` + Type string `json:"type"` + DevelopmentMode int `json:"development_mode"` + NameServers []string `json:"name_servers"` + OriginalNameServers []string `json:"original_name_servers"` +} + +func parseGetZonesResponse(response *http.Response) (*GetZonesResult, error) { + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + var parsedReponse GetZonesResult + if err := json.Unmarshal(body, &parsedReponse); err != nil { + return nil, err + } + return &parsedReponse, nil +} + +func exportZoneRequest(zoneID, authEmail, authKey string) (*http.Request, error) { + // construct the query + requestURI, err := url.Parse(cloudFlareURI) + if err != nil { + return nil, err + } + requestURI.Path = requestURI.Path + "zones/" + zoneID + "/dns_records/export" + request, err := http.NewRequest("GET", requestURI.String(), nil) + if err != nil { + return nil, err + } + addHeaders(request, authEmail, authKey) + return request, nil +}