package main import ( "bytes" "encoding/json" "fmt" "github.com/matryer/try" log "github.com/sirupsen/logrus" "github.com/urfave/cli" "gopkg.in/gomail.v2" "io/ioutil" "net/http" "os" "path" "regexp" "time" ) type Configuration struct { Username string `json:"name"` Email string `json:"email"` GandiKey string `json:"gandikey"` Domains []string `json:"domains"` RouterLogin string `json:"router_login"` RouterPassword string `json:"router_password"` RouterStatusUrl string `json:"router_status_url"` WanIPRegex string `json:"wan_ip_regex"` MailSettings *struct { Sender string `json:"sender"` To []string `json:"to"` SmtpHost string `json:"smtp_host"` SmtpLogin string `json:"smtp_login"` SmtpPassword string `json:"smtp_password"` SmtpPort int `json:"smtp_port"` } `json:"mail_settings"` } type DnsResponse struct { RecordType string `json:"rrset_type"` Ttl int `json:"rrset_ttl"` Name string `json:"rrset_name"` Values []string `json:"rrset_values"` } var ( configuration *Configuration configPath string ) const ( GandiARecordUrl = "https://dns.api.gandi.net/api/v5/domains/%s/records/@/A" ) func main() { app := cli.NewApp() app.Name = "dnsupdater" app.Usage = "Automatically update dns" app.Before = before app.Action = start app.Flags = []cli.Flag{ cli.StringFlag{ Name: "config, C", Usage: "path to config file", Value: "/config", EnvVar: "CONFIG_PATH", }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } func before(c *cli.Context) error { log.SetLevel(log.InfoLevel) configPath = path.Join(c.String("config"), "conf.json") if confPathExists, _ := exists(configPath); !confPathExists { return fmt.Errorf("config file does not exist : %s", configPath) } file, _ := os.Open(configPath) defer file.Close() decoder := json.NewDecoder(file) configuration = &Configuration{} err := decoder.Decode(configuration) if err != nil { return err } return nil } func start(c *cli.Context) error { if err := update(); err != nil { SendStatusMail(fmt.Sprintf("Error during update process : %s", err)) return err } return nil } func update() error { newIp, err := GetOutboundIP() if err != nil { return err } updatedDomains := "" for _, domain := range configuration.Domains { currentIp, err := GetCurrentIp(*configuration, domain) if err != nil { return err } if currentIp == newIp { continue } log.Infoln(fmt.Sprintf("%s -> Ip has changed %s -> %s", domain, currentIp, newIp)) err = SetCurrentIp(newIp, domain, *configuration) if err != nil { return err } updatedDomains += fmt.Sprintf("\t- %s\n", domain) } if updatedDomains != "" { SendStatusMail(fmt.Sprintf(`Home IP has changed : %s Dns updated successfully for domains %s`, newIp, updatedDomains)) } return nil } func SendStatusMail(messageText string) { settings := configuration.MailSettings if settings == nil { log.Warnln("no smtp settings defined > status mail not sent") return } d := gomail.NewDialer(settings.SmtpHost, settings.SmtpPort, settings.SmtpLogin, settings.SmtpPassword) s, err := d.Dial() if err != nil { log.Errorln(fmt.Sprintf("error while dialing smtp server : %v", err)) return } m := gomail.NewMessage() for _, r := range settings.To { m.SetHeader("From", settings.Sender) m.SetAddressHeader("To", r, "") m.SetHeader("Subject", "DnsUpdater Status") m.SetBody("text/plain", messageText) if err := gomail.Send(s, m); err != nil { log.Warnln(fmt.Sprintf("could not send email to %q: %v", r, err)) } m.Reset() } } func GetCurrentIp(configuration Configuration, domain string) (string, error) { var value string err := try.Do(func(attempt int) (bool, error) { var err error value, err = func(configuration Configuration, domain string) (string, error) { url := fmt.Sprintf(GandiARecordUrl, domain) req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } req.Header.Set("X-Api-Key", configuration.GandiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() decoder := json.NewDecoder(resp.Body) dnsresponse := DnsResponse{} err = decoder.Decode(&dnsresponse) if err != nil { return "", err } return dnsresponse.Values[0], nil }(configuration, domain) if err != nil { log.Warnln(fmt.Sprintf("Error while getting current ip from gandi : %v", err)) time.Sleep(30 * time.Second) } return attempt < 5, err }) if err != nil { return "", err } return value, nil } func SetCurrentIp(newip string, domain string, configuration Configuration) error { url := fmt.Sprintf(GandiARecordUrl, domain) log.Infoln("URL:>", url) var str = fmt.Sprintf("{\"rrset_ttl\": %d,\"rrset_values\": [\"%s\"]}", 600, newip) log.Infoln("json:", str) var jsonStr = []byte(str) req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonStr)) if err != nil { return err } req.Header.Set("X-Api-Key", configuration.GandiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() log.Debugln("response Status:", resp.Status) log.Debugln("response Headers:", resp.Header) body, _ := ioutil.ReadAll(resp.Body) log.Debugln("response Body:", string(body)) return nil } func GetOutboundIP() (string, error) { url := configuration.RouterStatusUrl if url == "" { url = "http://192.168.1.1/Status_Internet.live.asp" } req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } if configuration.RouterLogin != "" && configuration.RouterPassword != "" { req.SetBasicAuth(configuration.RouterLogin, configuration.RouterPassword) } res, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { return "", err } log.Debugln(string(body)) wanipregex := configuration.WanIPRegex if wanipregex == "" { wanipregex = "wan_ipaddr::([0-9.]*)}" } r := regexp.MustCompile(wanipregex) matches := r.FindStringSubmatch(string(body)) if len(matches) < 2 { return "", fmt.Errorf("unable to find WAN IP with regex %s in %s", wanipregex, string(body)) } return matches[1], nil } func exists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return true, err }