2020-10-26 14:32:00 +04:00

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
}