3 Commits

Author SHA1 Message Date
mkelcik
fa2e4426f4 Refactor 2023-05-04 17:39:59 +02:00
mkelcik
f911b9ff16 Refactor 2023-05-04 17:28:25 +02:00
mkelcik
ffd5253f59 Initial Notifiers implementation 2023-05-04 11:44:27 +02:00
7 changed files with 169 additions and 8 deletions

View File

@@ -18,6 +18,9 @@ Before run, you need configure this environment variables.
- `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record
- `CHECK_INTERVAL_SECONDS` - (optional) how often will the ip address of the records be checked (default: `300`)
- `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.me`, `1.1.1.1`
- `NOTIFIERS` - (optional) setting the notifier in case of an update of the dns record. Multiple entries are separated by commas. (default none). Example: `webhook@http://localhost/cloudflare-notification`
- Available(
- `webhook` - Call defined webhook. Example: `webhook@http://localhost/cloudflare-notification`
### Building from source

View File

@@ -17,6 +17,7 @@ const (
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
envKeyNotifiers = "NOTIFIERS"
)
type Config struct {
@@ -25,6 +26,7 @@ type Config struct {
ApiToken string
CloudflareZone string
OnChangeComment string
Notifiers []string
CheckInterval time.Duration
}
@@ -52,11 +54,12 @@ func NewConfig() Config {
}
return Config{
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
ApiToken: os.Getenv(envKeyCloudflareApiKey),
CloudflareZone: os.Getenv(envKeyCloudflareZone),
OnChangeComment: os.Getenv(envKeyOnChangeComment),
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
CheckInterval: time.Duration(checkInterval) * time.Second,
}
}

View File

@@ -2,10 +2,10 @@ package internal
import "strings"
func parseDNSToCheck(data string) []string {
func parseCommaDelimited(data string) []string {
out := make([]string, 0, strings.Count(data, ",")+1)
for _, dns := range strings.Split(data, ",") {
if w := strings.TrimSpace(dns); w != "" {
for _, item := range strings.Split(data, ",") {
if w := strings.TrimSpace(item); w != "" {
out = append(out, w)
}
}

View File

@@ -38,8 +38,8 @@ func Test_parseDNSToCheck(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseDNSToCheck(tt.args.data); !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseDNSToCheck() = %v, want %v", got, tt.want)
if got := parseCommaDelimited(tt.args.data); !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseCommaDelimited() = %v, want %v", got, tt.want)
}
})
}

17
main.go
View File

@@ -10,6 +10,7 @@ import (
"github.com/cloudflare/cloudflare-go"
"github.com/mkelcik/cloudflare-ddns-update/internal"
"github.com/mkelcik/cloudflare-ddns-update/notifications"
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
)
@@ -51,6 +52,8 @@ func main() {
log.Fatal(err)
}
notifiers := notifications.GetNotifiers(config.Notifiers)
// public ip resolver
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
@@ -85,9 +88,19 @@ func main() {
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
log.Printf("error updating dns record: %s", err)
} else {
log.Printf("Updated to `%s`", currentPublicIP)
continue
}
if err := notifiers.NotifyWithLog(ctx, notifications.Notification{
OldIp: net.ParseIP(dnsRecord.Content),
NewIp: currentPublicIP,
CheckedAt: time.Now(),
ResolverTag: resolverTag,
Domain: dnsRecord.Name,
}); err != nil {
log.Printf("errors in notifications: %s", err)
}
log.Printf("Updated to `%s`", currentPublicIP)
}
}
}

76
notifications/types.go Normal file
View File

@@ -0,0 +1,76 @@
package notifications
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
)
const (
configDelimiter = "@"
)
type Notifiers []Notifier
func (n Notifiers) NotifyWithLog(ctx context.Context, notification Notification) error {
var outErr error
for _, notifier := range n {
if err := notifier.Notify(ctx, notification); err != nil {
outErr = errors.Join(outErr, err)
continue
}
log.Printf("Notification sent via %s\n", notifier.Tag())
}
return outErr
}
type Notification struct {
OldIp net.IP `json:"old_ip,omitempty"`
NewIp net.IP `json:"new_ip"`
CheckedAt time.Time `json:"checked_at"`
ResolverTag string `json:"resolver_tag"`
Domain string `json:"domain"`
}
func (n Notification) ToSlice() []string {
return []string{n.OldIp.String(), n.NewIp.String(), n.CheckedAt.Format(time.RFC3339), n.ResolverTag, n.Domain}
}
var Available = map[string]func(string) (Notifier, error){
webhookTag: func(config string) (Notifier, error) {
parts := strings.Split(config, configDelimiter)
if len(parts) < 2 {
return nil, fmt.Errorf("wrong webhook config, missing url part")
}
return NewWebhookNotification(WebhookConfig{Url: parts[1]}, &http.Client{
Timeout: 10 * time.Second,
}), nil
},
}
type Notifier interface {
Tag() string
Notify(ctx context.Context, notification Notification) error
}
func GetNotifiers(tags []string) Notifiers {
out := Notifiers{}
for _, t := range tags {
if initFn, ok := Available[strings.Split(t, configDelimiter)[0]]; ok {
notifier, err := initFn(t)
if err != nil {
log.Println(err)
continue
}
out = append(out, notifier)
}
}
return out
}

66
notifications/webhook.go Normal file
View File

@@ -0,0 +1,66 @@
package notifications
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
const (
webhookTag = "webhook"
)
type Doer interface {
Do(*http.Request) (*http.Response, error)
}
type WebhookConfig struct {
Url string
}
type WebhookNotification struct {
config WebhookConfig
client Doer
}
func (w WebhookNotification) Tag() string {
return webhookTag
}
func NewWebhookNotification(config WebhookConfig, client Doer) *WebhookNotification {
return &WebhookNotification{config: config, client: client}
}
func (w WebhookNotification) getRequestBody(notification Notification) (io.Reader, error) {
out := bytes.NewBuffer(nil)
if err := json.NewEncoder(out).Encode(notification); err != nil {
return nil, fmt.Errorf("error encoding json notification body: %w", err)
}
return out, nil
}
func (w WebhookNotification) Notify(ctx context.Context, notification Notification) error {
body, err := w.getRequestBody(notification)
if err != nil {
return fmt.Errorf("WebhookNotification::NotifyWithLog error: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.config.Url, body)
if err != nil {
return fmt.Errorf("WebhookNotification::NotifyWithLog error creating request: %w", err)
}
resp, err := w.client.Do(req)
if err != nil {
return fmt.Errorf("WebhookNotification::NotifyWithLog error while sending notification: %w", err)
}
_ = resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("WebhookNotification::NotifyWithLog unexpected non 2xx code %d returned", resp.StatusCode)
}
return nil
}