aboutsummaryrefslogtreecommitdiff
path: root/nccli.go
diff options
context:
space:
mode:
Diffstat (limited to 'nccli.go')
-rw-r--r--nccli.go372
1 files changed, 372 insertions, 0 deletions
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)
+ }
+}