dnsupdater/main.go

288 lines
6.5 KiB
Go

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
cron := cron.New()
err := cron.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 := cron.Entries()
if len(nextRuns) > 0 {
log.Debug("Scheduled next run: " + nextRuns[0].Next.String())
}
})
if err != nil {
return err
}
log.Info("First run: " + cron.Entries()[0].Schedule.Next(time.Now()).String())
cron.Start()
// Graceful shut-down on SIGINT/SIGTERM
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
signal.Notify(interrupt, syscall.SIGTERM)
<-interrupt
cron.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 {
SetCurrentIp(newIp, domain, configuration)
}
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" <laurent@lehouerou.net>
From: "Laurent Le Houerou" <laurent@lehouerou.net>
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)
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))
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...))
}