You've already forked ddns-updater
Compare commits
3 Commits
v1.3.0
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa2e4426f4 | ||
|
|
f911b9ff16 | ||
|
|
ffd5253f59 |
@@ -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
|
- `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`)
|
- `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`
|
- `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
|
### Building from source
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const (
|
|||||||
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||||
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||||
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||||
|
envKeyNotifiers = "NOTIFIERS"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -25,6 +26,7 @@ type Config struct {
|
|||||||
ApiToken string
|
ApiToken string
|
||||||
CloudflareZone string
|
CloudflareZone string
|
||||||
OnChangeComment string
|
OnChangeComment string
|
||||||
|
Notifiers []string
|
||||||
CheckInterval time.Duration
|
CheckInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +54,12 @@ func NewConfig() Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
|
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||||
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||||
|
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package internal
|
|||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
func parseDNSToCheck(data string) []string {
|
func parseCommaDelimited(data string) []string {
|
||||||
out := make([]string, 0, strings.Count(data, ",")+1)
|
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||||
for _, dns := range strings.Split(data, ",") {
|
for _, item := range strings.Split(data, ",") {
|
||||||
if w := strings.TrimSpace(dns); w != "" {
|
if w := strings.TrimSpace(item); w != "" {
|
||||||
out = append(out, w)
|
out = append(out, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func Test_parseDNSToCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := parseDNSToCheck(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
if got := parseCommaDelimited(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("parseDNSToCheck() = %v, want %v", got, tt.want)
|
t.Errorf("parseCommaDelimited() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
17
main.go
17
main.go
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cloudflare/cloudflare-go"
|
"github.com/cloudflare/cloudflare-go"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||||
|
"github.com/mkelcik/cloudflare-ddns-update/notifications"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifiers := notifications.GetNotifiers(config.Notifiers)
|
||||||
|
|
||||||
// public ip resolver
|
// public ip resolver
|
||||||
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
||||||
|
|
||||||
@@ -85,9 +88,19 @@ func main() {
|
|||||||
|
|
||||||
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
|
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
|
||||||
log.Printf("error updating dns record: %s", err)
|
log.Printf("error updating dns record: %s", err)
|
||||||
} else {
|
continue
|
||||||
log.Printf("Updated to `%s`", currentPublicIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
76
notifications/types.go
Normal 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
66
notifications/webhook.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user