From 4445692f75e4e4072cc4940bcb9eff80ae5559f0 Mon Sep 17 00:00:00 2001 From: Kumar Damani Date: Mon, 6 Jun 2022 19:04:32 -0400 Subject: Initial commit --- nccli.go | 372 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 nccli.go (limited to 'nccli.go') diff --git a/nccli.go b/nccli.go new file mode 100644 index 0000000..4059bd1 --- /dev/null +++ b/nccli.go @@ -0,0 +1,372 @@ +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) + } +} -- cgit v1.2.3