207 lines
5.2 KiB
Go
207 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"gopkg.in/gomail.v2"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"time"
|
|
|
|
"gitlab.lehouerou.net/laurent/dnsupdater/internal/config"
|
|
"gitlab.lehouerou.net/laurent/dnsupdater/internal/dns/gandi"
|
|
"gitlab.lehouerou.net/laurent/dnsupdater/internal/outboundip"
|
|
"gitlab.lehouerou.net/laurent/dnsupdater/internal/outboundip/ddwrt"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/sloghuman"
|
|
"github.com/dghubble/sling"
|
|
"github.com/matryer/try"
|
|
"github.com/urfave/cli"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
var (
|
|
configuration config.Configuration
|
|
configPath string
|
|
base *sling.Sling
|
|
)
|
|
|
|
const (
|
|
GandiApiUrl = "https://dns.api.gandi.net/api/v5/"
|
|
GandiARecordUrl = "domains/%s/records/@/A"
|
|
)
|
|
|
|
var log = sloghuman.Make(os.Stdout)
|
|
|
|
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(context.Background(), "", slog.Error(err))
|
|
}
|
|
}
|
|
|
|
func before(c *cli.Context) error {
|
|
configPath = path.Join(c.String("config"), "conf.json")
|
|
file, err := os.Open(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error while opening config file: %v", err)
|
|
}
|
|
defer file.Close()
|
|
decoder := json.NewDecoder(file)
|
|
configuration = config.Configuration{}
|
|
err = decoder.Decode(&configuration)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
base = sling.New().Client(&http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}).Base(GandiApiUrl).
|
|
Set("X-Api-Key", configuration.GandiKey)
|
|
|
|
return nil
|
|
}
|
|
|
|
func start(*cli.Context) error {
|
|
ctx := context.Background()
|
|
outboundIpRequester := ddwrt.NewIpRequester(configuration.RouterStatusUrl,
|
|
configuration.RouterLogin,
|
|
configuration.RouterPassword)
|
|
if err := update(ctx, outboundIpRequester); err != nil {
|
|
SendStatusMail(ctx, fmt.Sprintf("Error during update process : %s", err))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func update(ctx context.Context, requester outboundip.IpRequester) error {
|
|
newIpResult := <-requester.GetOutboundIp()
|
|
|
|
if newIpResult.Error != nil {
|
|
return fmt.Errorf("getting outbound ip: %v", newIpResult.Error)
|
|
}
|
|
newIp := newIpResult.Ip
|
|
|
|
updatedDomains := ""
|
|
for _, domain := range configuration.Domains {
|
|
currentIp, err := GetCurrentIp(ctx, domain)
|
|
if err != nil {
|
|
return fmt.Errorf("getting current ip: %v", err)
|
|
}
|
|
|
|
if currentIp == newIp {
|
|
continue
|
|
}
|
|
log.Info(ctx, "ip has changed", slog.F("domain", domain), slog.F("current ip", currentIp), slog.F("new ip", newIp))
|
|
err = SetCurrentIp(newIp, domain)
|
|
if err != nil {
|
|
return fmt.Errorf("setting current ip: %v", err)
|
|
}
|
|
updatedDomains += fmt.Sprintf("\t- %s\n", domain)
|
|
}
|
|
|
|
if updatedDomains != "" {
|
|
SendStatusMail(ctx, fmt.Sprintf(`Home IP has changed : %s
|
|
Dns updated successfully for domains
|
|
%s`, newIp, updatedDomains))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SendStatusMail(ctx context.Context, messageText string) {
|
|
|
|
settings := configuration.MailSettings
|
|
if settings == nil {
|
|
log.Warn(ctx, "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.Error(ctx, "dialing smtp server", slog.Error(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.Warn(ctx, "could not send email", slog.F("recipient", r), slog.Error(err))
|
|
}
|
|
m.Reset()
|
|
}
|
|
}
|
|
|
|
func GetCurrentIp(ctx context.Context, domain string) (string, error) {
|
|
var value string
|
|
err := try.Do(func(attempt int) (bool, error) {
|
|
var err error
|
|
value, err = func(domain string) (string, error) {
|
|
dnsresponse := gandi.DnsResponse{}
|
|
resp, err := base.New().Get(fmt.Sprintf(GandiARecordUrl, domain)).
|
|
ReceiveSuccess(&dnsresponse)
|
|
if err != nil {
|
|
return "", fmt.Errorf("requesting gandi dns info: %v", err)
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
return "", fmt.Errorf("requesting gandi dns info, no 2xx http status code: %d", resp.StatusCode)
|
|
}
|
|
if len(dnsresponse.Values) == 0 {
|
|
return "", fmt.Errorf("no values in dnsresponse")
|
|
}
|
|
return dnsresponse.Values[0], nil
|
|
}(domain)
|
|
if err != nil {
|
|
log.Warn(ctx, "getting current ip from gandi", slog.Error(err))
|
|
time.Sleep(30 * time.Second)
|
|
}
|
|
return attempt < 5, err
|
|
})
|
|
if err != nil {
|
|
return "", xerrors.Errorf("getting current ip from gandi: %w", err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func SetCurrentIp(newip string, domain string) error {
|
|
resp, err := base.New().Put(fmt.Sprintf(GandiARecordUrl, domain)).
|
|
BodyJSON(struct {
|
|
TTL int `json:"rrset_ttl"`
|
|
Values []string `json:"rrset_values"`
|
|
}{
|
|
TTL: 600,
|
|
Values: []string{newip},
|
|
}).ReceiveSuccess(nil)
|
|
if err != nil {
|
|
return xerrors.Errorf("setting gandi dns info: %w", err)
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
|
return xerrors.Errorf("setting gandi dns info, no 2xx http status code: %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|