Skip to content

Commit

Permalink
feat(pp): add redaction facilities
Browse files Browse the repository at this point in the history
  • Loading branch information
favonia committed Jul 7, 2024
1 parent ece052d commit a837e1b
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 25 deletions.
2 changes: 1 addition & 1 deletion cmd/ddns/ddns.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func main() {

func realMain() int { //nolint:funlen
ppfmt := pp.New(os.Stdout)
if !config.ReadEmoji("EMOJI", &ppfmt) || !config.ReadQuiet("QUIET", &ppfmt) {
if !config.InitializePP(&ppfmt) {
ppfmt.Infof(pp.EmojiUserError, "Bye!")
return 1
}
Expand Down
39 changes: 31 additions & 8 deletions internal/api/cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,24 @@ func (h *CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
Type: ipNet.RecordType(),
})
if err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to retrieve records of %q: %v", domain.Describe(), err)
ppfmt.Warningf(pp.EmojiError, "Failed to retrieve records of %q: %s",
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)
return nil, false, false
}

rmap := map[string]netip.Addr{}
for i := range rs {
rmap[rs[i].ID], err = netip.ParseAddr(rs[i].Content)
if err != nil {
ppfmt.Warningf(pp.EmojiImpossible, "Failed to parse the IP address in records of %q: %v", domain.Describe(), err)
ppfmt.Warningf(pp.EmojiImpossible,
"Failed to parse the IP address in records of %q: %s",
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)
return nil, false, false
}
}
Expand All @@ -224,8 +233,13 @@ func (h *CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
}

if err := h.cf.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), id); err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %v",
ipNet.RecordType(), domain.Describe(), id, err)
ppfmt.Warningf(pp.EmojiError, "Failed to delete a stale %s record of %q (ID: %s): %s",
ipNet.RecordType(),
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
pp.Redact(ppfmt, pp.DNSResourceIDs, id, "redacted"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)

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

Expand Down Expand Up @@ -255,8 +269,13 @@ func (h *CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
}

if _, err := h.cf.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params); err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %v",
ipNet.RecordType(), domain.Describe(), id, err)
ppfmt.Warningf(pp.EmojiError, "Failed to update a stale %s record of %q (ID: %s): %s",
ipNet.RecordType(),
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
pp.Redact(ppfmt, pp.DNSResourceIDs, id, "redacted"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)

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

Expand Down Expand Up @@ -291,8 +310,12 @@ func (h *CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,

res, err := h.cf.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zone), params)
if err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %v",
ipNet.RecordType(), domain.Describe(), err)
ppfmt.Warningf(pp.EmojiError, "Failed to add a new %s record of %q: %s",
ipNet.RecordType(),
pp.Redact(ppfmt, pp.Domains, domain.Describe(), "(redacted)"),
// Redact error messages out of fear
pp.Redact(ppfmt, pp.Domains|pp.IPs|pp.DNSResourceIDs, err.Error(), "(error message redacted)"),
)

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

Expand Down
12 changes: 8 additions & 4 deletions internal/config/config_print.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ func (c *Config) Print(ppfmt pp.PP) {

section("Domains and IP providers:")
if c.Provider[ipnet.IP4] != nil {
item("IPv4 domains:", "%s", describeDomains(c.Domains[ipnet.IP4]))
item("IPv4 domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(c.Domains[ipnet.IP4]), "(redacted)"))
item("IPv4 provider:", "%s", provider.Name(c.Provider[ipnet.IP4]))
}
if c.Provider[ipnet.IP6] != nil {
item("IPv6 domains:", "%s", describeDomains(c.Domains[ipnet.IP6]))
item("IPv6 domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(c.Domains[ipnet.IP6]), "(redacted)"))
item("IPv6 provider:", "%s", provider.Name(c.Provider[ipnet.IP6]))
}

Expand All @@ -89,8 +91,10 @@ func (c *Config) Print(ppfmt pp.PP) {
item("TTL:", "%s", c.TTL.Describe())
{
_, inverseMap := getInverseMap(c.Proxied)
item("Proxied domains:", "%s", describeDomains(inverseMap[true]))
item("Unproxied domains:", "%s", describeDomains(inverseMap[false]))
item("Proxied domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(inverseMap[true]), "(redacted)"))
item("Unproxied domains:", "%s",
pp.Redact(ppfmt, pp.Domains, describeDomains(inverseMap[false]), "(redacted)"))
}
item("Record comment:", "%s", describeComment(c.RecordComment))

Expand Down
7 changes: 7 additions & 0 deletions internal/config/config_print_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ func TestPrintDefault(t *testing.T) {
mockPP.EXPECT().IncIndent().Return(mockPP),
mockPP.EXPECT().IncIndent().Return(innerMockPP),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 35 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 35 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv4 domains:", "(none)"),
printItem(innerMockPP, "IPv4 provider:", "cloudflare.trace"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 38 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 38 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv6 domains:", "(none)"),
printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
Expand All @@ -44,6 +46,7 @@ func TestPrintDefault(t *testing.T) {
printItem(innerMockPP, "Cache expiration:", "6h0m0s"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
printItem(innerMockPP, "TTL:", "1 (auto)"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 49 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 49 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "Proxied domains:", "(none)"),
printItem(innerMockPP, "Unproxied domains:", "(none)"),
printItem(innerMockPP, "Record comment:", "(empty)"),
Expand All @@ -68,8 +71,10 @@ func TestPrintValues(t *testing.T) {
mockPP.EXPECT().IncIndent().Return(mockPP),
mockPP.EXPECT().IncIndent().Return(innerMockPP),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Domains and IP providers:"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 74 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 74 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv4 domains:", "test4.org, *.test4.org"),
printItem(innerMockPP, "IPv4 provider:", "cloudflare.trace"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 77 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 77 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "IPv6 domains:", "test6.org, *.test6.org"),
printItem(innerMockPP, "IPv6 provider:", "cloudflare.trace"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Scheduling:"),
Expand All @@ -80,6 +85,7 @@ func TestPrintValues(t *testing.T) {
printItem(innerMockPP, "Cache expiration:", "6h0m0s"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
printItem(innerMockPP, "TTL:", "30000"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 88 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains

Check failure on line 88 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "Proxied domains:", "a, b"),
printItem(innerMockPP, "Unproxied domains:", "c, d"),
printItem(innerMockPP, "Record comment:", "\"Created by Cloudflare DDNS\""),
Expand Down Expand Up @@ -146,6 +152,7 @@ func TestPrintEmpty(t *testing.T) {
printItem(innerMockPP, "Cache expiration:", "0s"),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Parameters of new DNS records:"),
printItem(innerMockPP, "TTL:", "0"),
mockPP.EXPECT().ShouldRedact(pp.PrivateDataTypeDomains).Return(false),

Check failure on line 155 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: pp.PrivateDataTypeDomains (typecheck)

Check failure on line 155 in internal/config/config_print_test.go

View workflow job for this annotation

GitHub Actions / Test

undefined: pp.PrivateDataTypeDomains
printItem(innerMockPP, "Proxied domains:", "(none)"),
printItem(innerMockPP, "Unproxied domains:", "(none)"),
printItem(innerMockPP, "Record comment:", "(empty)"),
Expand Down
22 changes: 18 additions & 4 deletions internal/config/config_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"github.com/favonia/cloudflare-ddns/internal/provider"
)

func InitializePP(ppfmt *pp.PP) bool {
return ReadEmoji("EMOJI", ppfmt) && ReadQuiet("QUIET", ppfmt) && ReadRedaction("LOG_REDACTION", ppfmt)
}

// ReadEnv calls the relevant readers to read all relevant environment variables except TZ
// and update relevant fields. One should subsequently call [Config.NormalizeConfig]
// to maintain invariants across different fields.
Expand Down Expand Up @@ -112,9 +116,11 @@ func (c *Config) NormalizeConfig(ppfmt pp.PP) bool {
continue
}

ppfmt.Warningf(pp.EmojiUserWarning,
"Domain %q is ignored because it is only for %s but %s is disabled",
domain.Describe(), ipNet.Describe(), ipNet.Describe())
if !ppfmt.ShouldRedact(pp.Domains) {
ppfmt.Warningf(pp.EmojiUserWarning,
"Domain %q is ignored because it is only for %s but %s is disabled",
domain.Describe(), ipNet.Describe(), ipNet.Describe())
}
}
}

Expand All @@ -123,8 +129,16 @@ func (c *Config) NormalizeConfig(ppfmt pp.PP) bool {
if !ok {
return false
}
allProxied := true
for dom := range activeDomainSet {
proxiedMap[dom] = proxiedPred(dom)
proxied := proxiedPred(dom)
allProxied = allProxied && proxied
proxiedMap[dom] = proxied
}
// Warn about LOG_REDACTION=ip and PROXIED=false
if ppfmt.ShouldRedact(pp.IPs) && !allProxied {
ppfmt.Warningf(pp.EmojiUserWarning,
"Some domains are not proxied by Cloudflare; their DNS records can leak IP addresses")
}

// Part 3: override the old values
Expand Down
23 changes: 23 additions & 0 deletions internal/config/env_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@ func ReadQuiet(key string, ppfmt *pp.PP) bool {
return true
}

// ReadRedaction reads an environment variable as the redaction mask.
func ReadRedaction(key string, ppfmt *pp.PP) bool {
valRedaction := Getenv(key)
if valRedaction == "" {
return true
}

switch valRedaction {
case "min":
*ppfmt = (*ppfmt).SetRedactMask(pp.RedactNone)
return true
case "token":
*ppfmt = (*ppfmt).SetRedactMask(pp.RedactTokens)
return true
case "max":
*ppfmt = (*ppfmt).SetRedactMask(pp.RedactMaximum)
return true
default:
(*ppfmt).Errorf(pp.EmojiUserError, "%s (%q) is not a supported redaction mode", key, valRedaction)
return false
}
}

// ReadBool reads an environment variable as a boolean value.
func ReadBool(ppfmt pp.PP, key string, field *bool) bool {
val := Getenv(key)
Expand Down
76 changes: 76 additions & 0 deletions internal/mocks/mock_pp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions internal/pp/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ type PP interface {
// IsEnabledFor checks whether a message of a certain level will be displayed.
IsEnabledFor(v Verbosity) bool

// SetRedactMask sets the mask to determine the redaction.
SetRedactMask(m RedactMask) PP

// ShouldRedact(t) returns whether data of type t should be redacted.
ShouldRedact(t PrivateDataType) bool

// IncIndent returns a new pretty-printer with more indentation.
IncIndent() PP

Expand All @@ -29,3 +35,11 @@ type PP interface {
// Errorf formats and prints a message at the error level.
Errorf(emoji Emoji, format string, args ...any)
}

func Redact(pp PP, t PrivateDataType, orig string, redacted string) string {
if pp.ShouldRedact(t) {
return redacted
} else {
return orig
}
}
27 changes: 19 additions & 8 deletions internal/pp/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import (
)

type formatter struct {
writer io.Writer
emoji bool
indent int
verbosity Verbosity
writer io.Writer
emoji bool
indent int
verbosity Verbosity
redactionMask RedactMask
}

// New creates a new pretty printer.
func New(writer io.Writer) PP {
return formatter{
writer: writer,
emoji: true,
indent: 0,
verbosity: DefaultVerbosity,
writer: writer,
emoji: true,
indent: 0,
verbosity: DefaultVerbosity,
redactionMask: DefaultRedactMask,
}
}

Expand All @@ -37,6 +39,15 @@ func (f formatter) IsEnabledFor(v Verbosity) bool {
return v >= f.verbosity
}

func (f formatter) SetRedactMask(m RedactMask) PP {
f.redactionMask = m
return f
}

func (f formatter) ShouldRedact(t PrivateDataType) bool {
return f.redactionMask&RedactMask(t) > 0
}

func (f formatter) IncIndent() PP {
f.indent++
return f
Expand Down
Loading

0 comments on commit a837e1b

Please sign in to comment.