278 lines
6.5 KiB
Go
278 lines
6.5 KiB
Go
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
|
|
}
|