297 lines
6.6 KiB
Go
297 lines
6.6 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
|
|
|
|
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" <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)
|
|
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...))
|
|
}
|