Skip to content

Commit

Permalink
fix(monitor): use URL.JoinPath and redact the URL completely
Browse files Browse the repository at this point in the history
  • Loading branch information
favonia committed Aug 4, 2022
1 parent 4f13670 commit 1b9350f
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 125 deletions.
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (c *Config) Print(ppfmt pp.PP) {
if len(c.Monitors) > 0 {
ppfmt.Infof(pp.EmojiConfig, "Monitors:")
for _, m := range c.Monitors {
inner.Infof(pp.EmojiBullet, "%-17s %v", m.DescribeService()+":", m.DescribeBaseURL())
inner.Infof(pp.EmojiBullet, "%-17s (URL redacted)", m.DescribeService()+":")
}
} else {
ppfmt.Infof(pp.EmojiConfig, "Monitors: (none)")
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ func TestPrintMonitors(t *testing.T) {
innerMockPP.EXPECT().Infof(pp.EmojiBullet, "IP detection: %v", time.Second*5),
innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Record updating: %v", time.Second*30),
mockPP.EXPECT().Infof(pp.EmojiConfig, "Monitors:"),
innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-17s %v", "Healthchecks.io:", "http://user:xxxxx@host/path"),
innerMockPP.EXPECT().Infof(pp.EmojiBullet, "%-17s (URL redacted)", "Healthchecks.io:"),
)

m, ok := monitor.NewHealthChecks(mockPP, "http://user:pass@host/path")
Expand Down
55 changes: 33 additions & 22 deletions internal/config/env_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config_test

import (
"net/url"
"os"
"testing"
"time"
Expand Down Expand Up @@ -584,6 +585,13 @@ func TestReadCron(t *testing.T) {
}
}

func urlMustParse(t *testing.T, u string) *url.URL {
t.Helper()
url, err := url.Parse(u)
require.Nil(t, err)
return url
}

//nolint:paralleltest,funlen // paralleltest should not be used because environment vars are global
func TestReadHealthChecksURL(t *testing.T) {
key := keyPrefix + "HEALTHCHECKS"
Expand All @@ -608,10 +616,9 @@ func TestReadHealthChecksURL(t *testing.T) {
true, "https://hi.org/1234",
[]mon{},
[]mon{&monitor.HealthChecks{
BaseURL: "https://hi.org/1234",
RedactedBaseURL: "https://hi.org/1234",
Timeout: monitor.HealthChecksDefaultTimeout,
MaxRetries: monitor.HealthChecksDefaultMaxRetries,
BaseURL: urlMustParse(t, "https://hi.org/1234"),
Timeout: monitor.HealthChecksDefaultTimeout,
MaxRetries: monitor.HealthChecksDefaultMaxRetries,
}},
true,
nil,
Expand All @@ -620,30 +627,34 @@ func TestReadHealthChecksURL(t *testing.T) {
true, "https://me:pass@hi.org/1234",
[]mon{},
[]mon{&monitor.HealthChecks{
BaseURL: "https://me:pass@hi.org/1234",
RedactedBaseURL: "https://me:xxxxx@hi.org/1234",
Timeout: monitor.HealthChecksDefaultTimeout,
MaxRetries: monitor.HealthChecksDefaultMaxRetries,
BaseURL: urlMustParse(t, "https://me:pass@hi.org/1234"),
Timeout: monitor.HealthChecksDefaultTimeout,
MaxRetries: monitor.HealthChecksDefaultMaxRetries,
}},
true,
nil,
},
"illformed": {
true, "https://hi.org/1234?hello=123",
"fragment": {
true, "https://hi.org/1234#fragment",
[]mon{},
[]mon{&monitor.HealthChecks{
BaseURL: urlMustParse(t, "https://hi.org/1234#fragment"),
Timeout: monitor.HealthChecksDefaultTimeout,
MaxRetries: monitor.HealthChecksDefaultMaxRetries,
}},
true,
nil,
},
"query": {
true, "https://hi.org/1234?hello=123",
[]mon{},
false,
func(m *mocks.MockPP) {
m.EXPECT().Errorf(
pp.EmojiUserError,
"The URL %q does not look like a valid Healthchecks URL.",
"https://hi.org/1234?hello=123",
)
m.EXPECT().Errorf(
pp.EmojiUserError,
`A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc".`,
)
},
[]mon{&monitor.HealthChecks{
BaseURL: urlMustParse(t, "https://hi.org/1234?hello=123"),
Timeout: monitor.HealthChecksDefaultTimeout,
MaxRetries: monitor.HealthChecksDefaultMaxRetries,
}},
true,
nil,
},
} {
tc := tc
Expand Down
1 change: 0 additions & 1 deletion internal/monitor/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

type Monitor interface {
DescribeService() string
DescribeBaseURL() string
Success(context.Context, pp.PP) bool
Start(context.Context, pp.PP) bool
Failure(context.Context, pp.PP) bool
Expand Down
67 changes: 39 additions & 28 deletions internal/monitor/healthchecks.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"

"github.com/favonia/cloudflare-ddns/internal/pp"
)

type HealthChecks struct {
BaseURL string
RedactedBaseURL string
Timeout time.Duration
MaxRetries int
BaseURL *url.URL
Timeout time.Duration
MaxRetries int
}

const (
Expand All @@ -38,21 +38,20 @@ func SetHealthChecksMaxRetries(maxRetries int) HealthChecksOption {
func NewHealthChecks(ppfmt pp.PP, rawURL string, os ...HealthChecksOption) (Monitor, bool) {
url, err := url.Parse(rawURL)
if err != nil {
ppfmt.Errorf(pp.EmojiUserError, "Failed to parse the Healthchecks URL %q: %v", rawURL, err)
ppfmt.Errorf(pp.EmojiUserError, "Failed to parse the Healthchecks.io URL (redacted).")
return nil, false
}

if !(url.IsAbs() && url.Opaque == "" && url.Host != "" && url.Fragment == "" && !url.ForceQuery && url.RawQuery == "") { //nolint: lll
ppfmt.Errorf(pp.EmojiUserError, `The URL %q does not look like a valid Healthchecks URL.`, url.Redacted())
if !(url.IsAbs() && url.Opaque == "" && url.Host != "") { //nolint: lll
ppfmt.Errorf(pp.EmojiUserError, `The Healthchecks.io URL (redacted) does not look like a valid URL.`)
ppfmt.Errorf(pp.EmojiUserError, `A valid example is "https://hc-ping.com/01234567-0123-0123-0123-0123456789abc".`)
return nil, false
}

h := &HealthChecks{
BaseURL: url.String(),
RedactedBaseURL: url.Redacted(),
Timeout: HealthChecksDefaultTimeout,
MaxRetries: HealthChecksDefaultMaxRetries,
BaseURL: url,
Timeout: HealthChecksDefaultTimeout,
MaxRetries: HealthChecksDefaultMaxRetries,
}

for _, o := range os {
Expand All @@ -66,12 +65,15 @@ func (h *HealthChecks) DescribeService() string {
return "Healthchecks.io"
}

func (h *HealthChecks) DescribeBaseURL() string {
return h.RedactedBaseURL
}

//nolint: funlen
func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, url string, redatedURL string) bool {
func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string) bool {
url := h.BaseURL.JoinPath(endpoint)

endpointDescription := "default (root)"
if endpoint != "" {
endpointDescription = strconv.Quote(endpoint)
}

for retries := 0; retries < h.MaxRetries; retries++ {
if retries > 0 {
time.Sleep(time.Second << (retries - 1))
Expand All @@ -80,22 +82,28 @@ func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, url string, redate
ctx, cancel := context.WithTimeout(ctx, h.Timeout)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
ppfmt.Warningf(pp.EmojiImpossible, "Failed to prepare HTTP(S) request to %q: %v", redatedURL, err)
ppfmt.Warningf(pp.EmojiImpossible,
"Failed to prepare HTTP(S) request to the %s endpoint of Healthchecks.io: %v",
endpointDescription, err)
return false
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to send HTTP(S) request to %q: %v", redatedURL, err)
ppfmt.Warningf(pp.EmojiError,
"Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io: %v",
endpointDescription, err)
ppfmt.Infof(pp.EmojiRepeatOnce, "Trying again . . .")
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
ppfmt.Warningf(pp.EmojiError, "Failed to read HTTP(S) response from %q: %v", redatedURL, err)
ppfmt.Warningf(pp.EmojiError,
"Failed to read HTTP(S) response from the %s endpoint of Healthchecks.io: %v",
endpointDescription, err)
ppfmt.Infof(pp.EmojiRepeatOnce, "Trying again . . .")
continue
}
Expand All @@ -121,32 +129,35 @@ func (h *HealthChecks) ping(ctx context.Context, ppfmt pp.PP, url string, redate
if bodyAsString != "OK" {
ppfmt.Warningf(
pp.EmojiError,
"Failed to ping %q; got response code: %d %s",
redatedURL,
"Failed to ping the %s endpoint of Healthchecks.io; got response code: %d %s",
endpointDescription,
resp.StatusCode,
bodyAsString,
)
return false
}

ppfmt.Infof(pp.EmojiNotification, "Successfully pinged %q.", redatedURL)
ppfmt.Infof(pp.EmojiNotification, "Successfully pinged the %s endpoint of Healthchecks.io.", endpointDescription)
return true
}

ppfmt.Warningf(pp.EmojiError, "Failed to send HTTP(S) request to %q in %d time(s).", redatedURL, h.MaxRetries)
ppfmt.Warningf(
pp.EmojiError,
"Failed to send HTTP(S) request to the %s endpoint of Healthchecks.io in %d time(s).",
endpointDescription, h.MaxRetries)
return false
}

func (h *HealthChecks) Success(ctx context.Context, ppfmt pp.PP) bool {
return h.ping(ctx, ppfmt, h.BaseURL, h.RedactedBaseURL)
return h.ping(ctx, ppfmt, "")
}

func (h *HealthChecks) Start(ctx context.Context, ppfmt pp.PP) bool {
return h.ping(ctx, ppfmt, h.BaseURL+"/start", h.RedactedBaseURL+"/start")
return h.ping(ctx, ppfmt, "/start")
}

func (h *HealthChecks) Failure(ctx context.Context, ppfmt pp.PP) bool {
return h.ping(ctx, ppfmt, h.BaseURL+"/fail", h.RedactedBaseURL+"/fail")
return h.ping(ctx, ppfmt, "/fail")
}

func (h *HealthChecks) ExitStatus(ctx context.Context, ppfmt pp.PP, code int) bool {
Expand All @@ -155,5 +166,5 @@ func (h *HealthChecks) ExitStatus(ctx context.Context, ppfmt pp.PP, code int) bo
return false
}

return h.ping(ctx, ppfmt, fmt.Sprintf("%s/%d", h.BaseURL, code), fmt.Sprintf("%s/%d", h.RedactedBaseURL, code))
return h.ping(ctx, ppfmt, fmt.Sprintf("/%d", code))
}

0 comments on commit 1b9350f

Please sign in to comment.