package main import ( "bufio" "encoding/xml" "flag" "fmt" "io/ioutil" "log" "net/http" "os" "strconv" "strings" ) var ( verbose = false url string ) // Based on: https://www.namecheap.com/support/api/methods/domains-dns/set-hosts/ type SetResponseObj struct { XMLName xml.Name `xml:"ApiResponse"` // Based on: https://www.namecheap.com/support/api/intro/ Status string `xml:"Status,attr"` Type string `xml:"RequestedCommand"` DNSResultObj DNSResultObj `xml:"CommandResponse>DomainDNSSetHostsResult"` } type DNSResultObj struct { XMLName xml.Name `xml:"DomainDNSSetHostsResult"` Domain string `xml:"Domain,attr"` IsSuccess bool `xml:"IsSuccess,attr"` } //Based on: https://www.namecheap.com/support/api/methods/domains-dns/get-hosts/ type GetResponseObj struct { XMLName xml.Name `xml:"ApiResponse"` // Based on: https://www.namecheap.com/support/api/intro/ Status string `xml:"Status,attr"` Type string `xml:"RequestedCommand"` Hosts []Host `xml:"CommandResponse>DomainDNSGetHostsResult>host"` } type Host struct { XMLName xml.Name `xml:"host"` Type string `xml:"Type,attr"` Name string `xml:"Name,attr"` Address string `xml:"Address,attr"` MXPref int `xml:"MXPref,attr"` TTL int `xml:"TTL,attr"` } // Helper functions // // A helper to check if a env var is defined. If not defined, show error. // Args: key - the env var to check for // Returns if exists, the value of the var func getEnv(key string) string { value := os.Getenv(key) if len(value) == 0 { log.Fatalf("Env var %s is required and missing.", key) } if verbose { fmt.Println(key, value) } return value } func loadConfig() { url = "https://api.namecheap.com/xml.response?ApiUser=" + getEnv("API_USER") + "&ApiKey=" + getEnv("API_KEY") + "&UserName=" + getEnv("API_USER") + "&ClientIp=" + getEnv("ACCESS_IP") + "&SLD=" + getEnv("DOMAIN_MAIN") + "&TLD=" + getEnv("DOMAIN_END") } func logRecords(hosts []Host) { fmt.Println("================================") fmt.Println("Number of DNS records:", len(hosts)) for i, host := range hosts { fmt.Println( "[", i, "]", host.Type, host.Name, host.Address, host.MXPref, host.TTL, ) } fmt.Println("================================") } func makeReq(endPoint string, extraParams ...string) []byte { apiUrl := url + "&Command=namecheap.domains.dns." + endPoint for _, param := range extraParams { apiUrl += param } if verbose { fmt.Println("Using api url:", apiUrl) } var ( response *http.Response err error ) switch endPoint { case "gethosts": response, err = http.Get(apiUrl) case "sethosts": response, err = http.Post(apiUrl, "application/xml", nil) default: log.Fatalln("Error: Invalid request type specified.") } if err != nil { log.Fatalln(err) } responseData, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatalln(err) } return responseData } func normalizeUserChoices(userChoicesStrs []string, maxVal int) []int { var chosenInts []int for _, choice := range userChoicesStrs { if len(strings.TrimSpace(choice)) == 0 { continue } val, err := strconv.Atoi(choice) if err != nil { log.Fatalln("Error: Bad user input detected. Please only enter numbers.") } // validate if (val >= 0) && (val < maxVal) { chosenInts = append(chosenInts, val) } else { log.Fatalln("Error: Bad user input detected. One of the numbers is not correct.") } } return chosenInts } // If an existing (Name, Type) exists => update the first match... func updateHostRecordSet(hostRecords []Host, newHost Host) { for i, host := range hostRecords { if (host.Name == newHost.Name) && (host.Type == newHost.Type) { hostRecords[i].Address = newHost.Address hostRecords[i].MXPref = newHost.MXPref hostRecords[i].TTL = newHost.TTL } } } func updateNamecheapRecords(urlEncodedHosts string) { // make the api request to namecheap responseData := makeReq("sethosts", urlEncodedHosts) var responseObj SetResponseObj xml.Unmarshal(responseData, &responseObj) // validate if responseObj.Status != "OK" { log.Fatalln(string(responseData)) log.Fatalln("Error: Api request failed. Exiting.") } if !responseObj.DNSResultObj.IsSuccess { log.Fatalln(string(responseData)) log.Fatalln("Error: Something went wrong updating DNS records for:", responseObj.DNSResultObj.Domain) } if verbose { fmt.Println("Success updating DNS records for:", responseObj.DNSResultObj.Domain) } } func urlEncodeHosts(hosts []Host) string { var res string for i, host := range hosts { res += fmt.Sprintf("&HostName%d=%s&RecordType%d=%s&Address%d=%s&MXPref%d=%d&TTL%d=%d", i+1, host.Name, i+1, host.Type, i+1, host.Address, i+1, host.MXPref, i+1, host.TTL, ) } return res } func userInputHandler() []string { var chosen []string for { fmt.Println("Please enter the number next to the DNS record you want to remove: ") fmt.Println("Each number should be seperated by a space:") scanner := bufio.NewScanner(os.Stdin) scanner.Scan() err := scanner.Err() if err != nil { log.Fatal(err) } text := strings.TrimSpace(scanner.Text()) if len(text) == 0 { fmt.Println("Nothing chosen. Exiting.") os.Exit(0) } // split by "," chosen = strings.Split(text, " ") fmt.Println("You chose", chosen, ". Are you sure you want to remove these? Y/n:") scanner2 := bufio.NewScanner(os.Stdin) scanner2.Scan() err2 := scanner2.Err() if err2 != nil { log.Fatal(err2) } confirm := scanner2.Text() if confirm == "Y" { break } } return chosen } // Main functions // func getAllRecords() []Host { responseData := makeReq("gethosts") var responseObj GetResponseObj xml.Unmarshal(responseData, &responseObj) // validate if responseObj.Status != "OK" { log.Fatalln(string(responseData)) log.Fatalln("Error: Api request failed. Exiting.") } return responseObj.Hosts } func addRecord(rName string, rType string, rAddress string, rMXPref int, rTTL int) { // we need to get the records before we can add to it. var hostRecords = getAllRecords() if verbose { logRecords(hostRecords) } hostRecords = append(hostRecords, Host{ Name: rName, Type: rType, Address: rAddress, MXPref: rMXPref, TTL: rTTL, }) // encode all hosts into a url urlEncodedHosts := urlEncodeHosts(hostRecords) updateNamecheapRecords(urlEncodedHosts) } func removeRecord() { // we need to get the records before we can remove one var hostRecords = getAllRecords() logRecords(hostRecords) // get user input for which ones to delete userChoicesStrs := userInputHandler() userChoicesInts := normalizeUserChoices(userChoicesStrs, len(hostRecords)) // contains the desired set of host records. var newHostRecords []Host for i, host := range hostRecords { found := false for _, rmIdx := range userChoicesInts { if i == rmIdx { found = true break } } if !found { newHostRecords = append(newHostRecords, host) } } // encode all hosts into a url urlEncodedHosts := urlEncodeHosts(newHostRecords) updateNamecheapRecords(urlEncodedHosts) } func updateRecord(rName string, rType string, rAddress string, rMXPref int, rTTL int) { // we need to get the records before we can update it. var hostRecords = getAllRecords() if verbose { logRecords(hostRecords) } updateHostRecordSet( hostRecords, Host{ Name: rName, Type: rType, Address: rAddress, MXPref: rMXPref, TTL: rTTL, }, ) // encode all hosts into a url urlEncodedHosts := urlEncodeHosts(hostRecords) updateNamecheapRecords(urlEncodedHosts) } func main() { // Subcommads: list, add, remove, update // define cli args for the "list" subcommand: listCmd := flag.NewFlagSet("list", flag.ExitOnError) listCmd.BoolVar(&verbose, "v", false, "Verbosity. More output. Default=false.") // define cli args for the "add" subcommand: addCmd := flag.NewFlagSet("add", flag.ExitOnError) addName := addCmd.String("name", "", "Name of DNS record.") addType := addCmd.String("type", "", "Type of DNS record.") addAddress := addCmd.String("value", "", "Value of the DNS record.") addMXPref := addCmd.Int("priority", 0, "Value of the DNS record.") addTTL := addCmd.Int("ttl", 1800, "TTL of the DNS record.") addCmd.BoolVar(&verbose, "v", false, "Verbosity. More output. Default=false.") // define cli args for the "list" subcommand: removeCmd := flag.NewFlagSet("remove", flag.ExitOnError) removeCmd.BoolVar(&verbose, "v", false, "Verbosity. More output. Default=false.") // define cli args for the "update" subcommand: updateCmd := flag.NewFlagSet("update", flag.ExitOnError) updateName := updateCmd.String("name", "", "Name of DNS record.") updateType := updateCmd.String("type", "", "Type of DNS record.") updateAddress := updateCmd.String("value", "", "Value of the DNS record.") updateMXPref := updateCmd.Int("priority", 0, "Value of the DNS record.") updateTTL := updateCmd.Int("ttl", 1800, "TTL of the DNS record.") updateCmd.BoolVar(&verbose, "v", false, "Verbosity. More output. Default=false.") subCmdErrorMsg := "Error: Expected one of: list, add, remove, or update subcommands." addUpdateErrorMsg := "Error: Missing options. Please use -h to see the list of options." if len(os.Args) < 2 { log.Fatalln(subCmdErrorMsg) } switch os.Args[1] { case "list": listCmd.Parse(os.Args[2:]) loadConfig() logRecords(getAllRecords()) case "add": addCmd.Parse(os.Args[2:]) if *addName == "" || *addType == "" || *addAddress == "" { log.Fatalln(addUpdateErrorMsg) } loadConfig() addRecord(*addName, *addType, *addAddress, *addMXPref, *addTTL) case "remove": removeCmd.Parse(os.Args[2:]) loadConfig() removeRecord() case "update": updateCmd.Parse(os.Args[2:]) if *updateName == "" || *updateType == "" || *updateAddress == "" { log.Fatalln(addUpdateErrorMsg) } loadConfig() updateRecord(*updateName, *updateType, *updateAddress, *updateMXPref, *updateTTL) default: log.Fatalln(subCmdErrorMsg) } }