package main import ( "bytes" "encoding/json" "fmt" "github.com/robfig/cron" log "github.com/sirupsen/logrus" "github.com/urfave/cli" "io/ioutil" "net/http" "net/smtp" "os" "os/signal" "path" "strconv" "syscall" "time" ) type Configuration struct { Username string `json:"name"` Email string `json:"email"` GandiKey string `json:"gandikey"` Domains []string `json:"domains"` } type DnsResponse struct { RecordType string `json:"rrset_type"` Ttl int `json:"rrset_ttl"` Name string `json:"rrset_name"` Values []string `json:"rrset_values"` } var ( configPath string scheduleSpec string ) var version = "master" var commit = "unknown" var date = "unknown" func main() { app := cli.NewApp() app.Name = "dnsupdater" app.Version = version + " - " + commit + " - " + date 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/conf.json", EnvVar: "CONFIG_PATH", }, cli.IntFlag{ Name: "interval, i", Usage: "poll interval (in seconds)", Value: 300, EnvVar: "DNSUPDATER_POLL_INTERVAL", }, cli.StringFlag{ Name: "schedule, s", Usage: "the cron expression which defines when to update", EnvVar: "DNSUPDATER_SCHEDULE", }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } func before(c *cli.Context) error { if c.GlobalBool("debug") { log.SetLevel(log.DebugLevel) } pollingSet := c.IsSet("interval") cronSet := c.IsSet("schedule") if pollingSet && cronSet { log.Fatal("Only schedule or interval can be defined, not both.") } else if cronSet { scheduleSpec = c.String("schedule") } else { scheduleSpec = "@every " + strconv.Itoa(c.Int("interval")) + "s" } return nil } func start(c *cli.Context) error { tryLockSem := make(chan bool, 1) tryLockSem <- true cr := cron.New() err := cr.AddFunc( scheduleSpec, func() { select { case v := <-tryLockSem: defer func() { tryLockSem <- v }() if err := update(c); err != nil { log.Println(err) SendStatusMail(fmt.Sprintf("Error during update process : %s", err)) } default: log.Debug("Skipped another update already running.") } nextRuns := cr.Entries() if len(nextRuns) > 0 { log.Debug("Scheduled next run: " + nextRuns[0].Next.String()) } }) if err != nil { return err } log.Info("First run: " + cr.Entries()[0].Schedule.Next(time.Now()).String()) cr.Start() // Graceful shut-down on SIGINT/SIGTERM interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) signal.Notify(interrupt, syscall.SIGTERM) <-interrupt cr.Stop() log.Info("Waiting for running update to be finished...") <-tryLockSem os.Exit(1) return nil } func update(c *cli.Context) error { configPath = c.String("config") if confPathExists, _ := exists(configPath); !confPathExists { configPath = "./data" } file, _ := os.Open(path.Join(configPath, "conf.json")) defer file.Close() decoder := json.NewDecoder(file) configuration := Configuration{} err := decoder.Decode(&configuration) currentIp, err := GetCurrentIp(configuration) if err != nil { return err } newIp, err := GetOutboundIP() if err != nil { return err } if currentIp == newIp { return nil } Info("Ip has changed %s -> %s", currentIp, newIp) for _, domain := range configuration.Domains { err = SetCurrentIp(newIp, domain, configuration) if err != nil { return err } } SendStatusMail(fmt.Sprintf(`Home IP has changed : %s -> %s Dns updated successfully`, currentIp, newIp)) return nil } func SendStatusMail(messageText string) { // user we are authorizing as from := "laurent@lehouerou.net" // use we are sending email to to := "laurent@lehouerou.net" // server we are authorized to send email through host := "smtp.fastmail.com" // Create the authentication for the SendMail() // using PlainText, but other authentication methods are encouraged auth := smtp.PlainAuth("", from, "c9bd8fb8l4bhs2f8", host) // NOTE: Using the backtick here ` works like a heredoc, which is why all the // rest of the lines are forced to the beginning of the line, otherwise the // formatting is wrong for the RFC 822 style message := fmt.Sprintf(`To: "Laurent Le Houerou" From: "Laurent Le Houerou" Subject: DnsUpdater Status %s `, messageText) if err := smtp.SendMail(host+":587", auth, from, []string{to}, []byte(message)); err != nil { fmt.Println("Error SendMail: ", err) } else { fmt.Println("Email Sent!") } } func GetCurrentIp(configuration Configuration) (string, error) { url := "https://dns.api.gandi.net/api/v5/domains/lehouerou.net/records/@/A" 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 } func SetCurrentIp(newip string, domain string, configuration Configuration) error { url := fmt.Sprintf("https://dns.api.gandi.net/api/v5/domains/%s/records/@/A", domain) fmt.Println("URL:>", url) var str = fmt.Sprintf("{\"rrset_ttl\": %d,\"rrset_values\": [\"%s\"]}", 600, newip) fmt.Println("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() fmt.Println("response Status:", resp.Status) fmt.Println("response Headers:", resp.Header) body, _ := ioutil.ReadAll(resp.Body) fmt.Println("response Body:", string(body)) return nil } func GetOutboundIP() (string, error) { url := "https://api.ipify.org" req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } 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 } return string(body), 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 } func Info(format string, args ...interface{}) { fmt.Printf("\x1b[34;1m%s\x1b[0m\n", fmt.Sprintf(format, args...)) }