Skip to content

Commit

Permalink
feat(api): support wildcard domains (#94)
Browse files Browse the repository at this point in the history
* feat(api): support wildcard domains

* fix(api): revert the complicated domain name splitting
  • Loading branch information
favonia authored Oct 18, 2021
1 parent f4ec041 commit feafcf4
Show file tree
Hide file tree
Showing 15 changed files with 602 additions and 274 deletions.
7 changes: 4 additions & 3 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ A small and fast DDNS updater for Cloudflare.
* Ability to update multiple domains across different zones.
* Ability to enable or disable IPv4 and IPv6 individually.
* Support of internationalized domain names.
* Support of wildcard domain names (_e.g._, `*.example.org`).
* Ability to remove stale records or choose to remove records on exit/stop.
* Ability to obtain IP addresses from Cloudflare, ipify, or local network interfaces.
* Support of timezone and Cron expressions.
Expand Down Expand Up @@ -273,10 +274,10 @@ In most cases, `CF_ACCOUNT_ID` is not needed.

| Name | Valid Values | Meaning | Required? | Default Value |
| ---- | ------------ | ------- | --------- | ------------- |
| `DOMAINS` | Comma-separated fully qualified domain names | The domains this tool should manage | (See below) | N/A
| `IP4_DOMAINS` | Comma-separated fully qualified domain names | The domains this tool should manage for `A` records | (See below) | N/A
| `DOMAINS` | Comma-separated fully qualified domain names or wildcard domain names | The domains this tool should manage | (See below) | N/A
| `IP4_DOMAINS` | Comma-separated fully qualified domain names or wildcard domain names | The domains this tool should manage for `A` records | (See below) | N/A
| `IP4_POLICY` | `cloudflare`, `ipify`, `local`, and `unmanaged` | How to detect IPv4 addresses. (See below) | No | `cloudflare`
| `IP6_DOMAINS` | Comma-separated fully qualified domain names | The domains this tool should manage for `AAAA` records | (See below) | N/A
| `IP6_DOMAINS` | Comma-separated fully qualified domain names or wildcard domain names | The domains this tool should manage for `AAAA` records | (See below) | N/A
| `IP6_POLICY` | `cloudflare`, `ipify`, `local`, and `unmanaged` | How to detect IPv6 addresses. (See below) | No | `cloudflare`

> <details>
Expand Down
20 changes: 16 additions & 4 deletions internal/api/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,25 @@ import (
"github.com/favonia/cloudflare-ddns/internal/pp"
)

type DomainSplitter interface {
IsValid() bool
ZoneNameASCII() string
Next()
}

type Domain interface {
DNSNameASCII() string
Describe() string
Split() DomainSplitter
}

//go:generate mockgen -destination=../mocks/mock_api.go -package=mocks . Handle

type Handle interface {
ListRecords(ctx context.Context, ppfmt pp.PP, domain FQDN, ipNet ipnet.Type) (map[string]net.IP, bool)
DeleteRecord(ctx context.Context, ppfmt pp.PP, domain FQDN, ipNet ipnet.Type, id string) bool
UpdateRecord(ctx context.Context, ppfmt pp.PP, domain FQDN, ipNet ipnet.Type, id string, ip net.IP) bool
CreateRecord(ctx context.Context, ppfmt pp.PP, domain FQDN, ipNet ipnet.Type,
ListRecords(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type) (map[string]net.IP, bool)
DeleteRecord(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type, id string) bool
UpdateRecord(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type, id string, ip net.IP) bool
CreateRecord(ctx context.Context, ppfmt pp.PP, domain Domain, ipNet ipnet.Type,
ip net.IP, ttl TTL, proxied bool) (string, bool)
FlushCache()
}
40 changes: 20 additions & 20 deletions internal/api/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ func (h *CloudflareHandle) ActiveZones(ctx context.Context, ppfmt pp.PP, name st
return ids, true
}

func (h *CloudflareHandle) ZoneOfDomain(ctx context.Context, ppfmt pp.PP, domain FQDN) (string, bool) {
if id, found := h.cache.zoneOfDomain.Get(domain.ToASCII()); found {
func (h *CloudflareHandle) ZoneOfDomain(ctx context.Context, ppfmt pp.PP, domain Domain) (string, bool) {
if id, found := h.cache.zoneOfDomain.Get(domain.DNSNameASCII()); found {
return id.(string), true
}

zoneSearch:
for s := NewFQDNSplitter(domain); s.IsValid(); s.Next() {
zoneName := s.Suffix()
for s := domain.Split(); s.IsValid(); s.Next() {
zoneName := s.ZoneNameASCII()
zones, ok := h.ActiveZones(ctx, ppfmt, zoneName)
if !ok {
return "", false
Expand All @@ -119,7 +119,7 @@ zoneSearch:
case 0: // len(zones) == 0
continue zoneSearch
case 1: // len(zones) == 1
h.cache.zoneOfDomain.SetDefault(domain.ToASCII(), zones[0])
h.cache.zoneOfDomain.SetDefault(domain.DNSNameASCII(), zones[0])
return zones[0], true
default: // len(zones) > 1
ppfmt.Warningf(pp.EmojiImpossible,
Expand All @@ -133,8 +133,8 @@ zoneSearch:
}

func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
domain FQDN, ipNet ipnet.Type) (map[string]net.IP, bool) {
if rmap, found := h.cache.listRecords[ipNet].Get(domain.ToASCII()); found {
domain Domain, ipNet ipnet.Type) (map[string]net.IP, bool) {
if rmap, found := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); found {
return rmap.(map[string]net.IP), true
}

Expand All @@ -145,7 +145,7 @@ func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,

//nolint:exhaustivestruct // Other fields are intentionally unspecified
rs, err := h.cf.DNSRecords(ctx, zone, cloudflare.DNSRecord{
Name: domain.ToASCII(),
Name: domain.DNSNameASCII(),
Type: ipNet.RecordType(),
})
if err != nil {
Expand All @@ -158,13 +158,13 @@ func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
rmap[rs[i].ID] = net.ParseIP(rs[i].Content)
}

h.cache.listRecords[ipNet].SetDefault(domain.ToASCII(), rmap)
h.cache.listRecords[ipNet].SetDefault(domain.DNSNameASCII(), rmap)

return rmap, true
}

func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
domain FQDN, ipNet ipnet.Type, id string) bool {
domain Domain, ipNet ipnet.Type, id string) bool {
zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
if !ok {
return false
Expand All @@ -174,28 +174,28 @@ func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
ppfmt.Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %v",
ipNet.RecordType(), domain.Describe(), id, err)

h.cache.listRecords[ipNet].Delete(domain.ToASCII())
h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())

return false
}

if rmap, found := h.cache.listRecords[ipNet].Get(domain.ToASCII()); found {
if rmap, found := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); found {
delete(rmap.(map[string]net.IP), id)
}

return true
}

func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
domain FQDN, ipNet ipnet.Type, id string, ip net.IP) bool {
domain Domain, ipNet ipnet.Type, id string, ip net.IP) bool {
zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
if !ok {
return false
}

//nolint:exhaustivestruct // Other fields are intentionally omitted
payload := cloudflare.DNSRecord{
Name: domain.ToASCII(),
Name: domain.DNSNameASCII(),
Type: ipNet.RecordType(),
Content: ip.String(),
}
Expand All @@ -204,28 +204,28 @@ func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
ppfmt.Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %v",
ipNet.RecordType(), domain.Describe(), id, err)

h.cache.listRecords[ipNet].Delete(domain.ToASCII())
h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())

return false
}

if rmap, found := h.cache.listRecords[ipNet].Get(domain.ToASCII()); found {
if rmap, found := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); found {
rmap.(map[string]net.IP)[id] = ip
}

return true
}

func (h *CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,
domain FQDN, ipNet ipnet.Type, ip net.IP, ttl TTL, proxied bool) (string, bool) {
domain Domain, ipNet ipnet.Type, ip net.IP, ttl TTL, proxied bool) (string, bool) {
zone, ok := h.ZoneOfDomain(ctx, ppfmt, domain)
if !ok {
return "", false
}

//nolint:exhaustivestruct // Other fields are intentionally omitted
payload := cloudflare.DNSRecord{
Name: domain.ToASCII(),
Name: domain.DNSNameASCII(),
Type: ipNet.RecordType(),
Content: ip.String(),
TTL: ttl.Int(),
Expand All @@ -237,12 +237,12 @@ func (h *CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,
ppfmt.Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %v",
ipNet.RecordType(), domain.Describe(), err)

h.cache.listRecords[ipNet].Delete(domain.ToASCII())
h.cache.listRecords[ipNet].Delete(domain.DNSNameASCII())

return "", false
}

if rmap, found := h.cache.listRecords[ipNet].Get(domain.ToASCII()); found {
if rmap, found := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); found {
rmap.(map[string]net.IP)[res.Result.ID] = ip
}

Expand Down
Loading

0 comments on commit feafcf4

Please sign in to comment.