From 28167403a46b71de8a3eb94920ca0d5edcce8cf1 Mon Sep 17 00:00:00 2001 From: SimJunYou Date: Thu, 19 Sep 2024 17:43:43 +0800 Subject: [PATCH 1/2] Cover edge case when Pihole's privacy settings are enabled --- internal/feed/pihole.go | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/internal/feed/pihole.go b/internal/feed/pihole.go index 3849267..5a4b7f8 100644 --- a/internal/feed/pihole.go +++ b/internal/feed/pihole.go @@ -18,6 +18,17 @@ type piholeStatsResponse struct { DomainsBlocked int `json:"domains_being_blocked"` } +// If user has some level of privacy enabled on Pihole, `json:"top_ads"` is an empty array +// Use alternate struct without that field to avoid error when unmarshalling +type piholeStatsResponsePrivate struct { + TotalQueries int `json:"dns_queries_today"` + QueriesSeries map[int64]int `json:"domains_over_time"` + BlockedQueries int `json:"ads_blocked_today"` + BlockedSeries map[int64]int `json:"ads_over_time"` + BlockedPercentage float64 `json:"ads_percentage_today"` + DomainsBlocked int `json:"domains_being_blocked"` +} + func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { if token == "" { return nil, errors.New("missing API token") @@ -31,13 +42,27 @@ func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { if err != nil { return nil, err } - + responseJson, err := decodeJsonFromRequest[piholeStatsResponse](defaultClient, request) - + if err != nil { - return nil, err + // Refer to piholeStatsResponsePrivate above + responseJsonPriv, err := + decodeJsonFromRequest[piholeStatsResponsePrivate](defaultClient, request) + if err != nil { + return nil, err + } + + // Copy the results back to responseJson, leaving the TopBlockedDomains field empty + responseJson.TotalQueries = responseJsonPriv.TotalQueries + responseJson.QueriesSeries = responseJsonPriv.QueriesSeries + responseJson.BlockedQueries = responseJsonPriv.BlockedQueries + responseJson.BlockedSeries = responseJsonPriv.BlockedSeries + responseJson.BlockedPercentage = responseJsonPriv.BlockedPercentage + responseJson.TopBlockedDomains = make(map[string]int) + responseJson.DomainsBlocked = responseJsonPriv.DomainsBlocked } - + stats := &DNSStats{ TotalQueries: responseJson.TotalQueries, BlockedQueries: responseJson.BlockedQueries, From 672547cd07d0620ae988ec2ff96ac298bcb07778 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:24:44 +0100 Subject: [PATCH 2/2] Switch to using custom type for pihole's top blocked domains --- internal/feed/pihole.go | 62 +++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/internal/feed/pihole.go b/internal/feed/pihole.go index 5a4b7f8..3c7f1b5 100644 --- a/internal/feed/pihole.go +++ b/internal/feed/pihole.go @@ -1,6 +1,7 @@ package feed import ( + "encoding/json" "errors" "log/slog" "net/http" @@ -9,24 +10,33 @@ import ( ) type piholeStatsResponse struct { - TotalQueries int `json:"dns_queries_today"` - QueriesSeries map[int64]int `json:"domains_over_time"` - BlockedQueries int `json:"ads_blocked_today"` - BlockedSeries map[int64]int `json:"ads_over_time"` - BlockedPercentage float64 `json:"ads_percentage_today"` - TopBlockedDomains map[string]int `json:"top_ads"` - DomainsBlocked int `json:"domains_being_blocked"` + TotalQueries int `json:"dns_queries_today"` + QueriesSeries map[int64]int `json:"domains_over_time"` + BlockedQueries int `json:"ads_blocked_today"` + BlockedSeries map[int64]int `json:"ads_over_time"` + BlockedPercentage float64 `json:"ads_percentage_today"` + TopBlockedDomains piholeTopBlockedDomains `json:"top_ads"` + DomainsBlocked int `json:"domains_being_blocked"` } // If user has some level of privacy enabled on Pihole, `json:"top_ads"` is an empty array -// Use alternate struct without that field to avoid error when unmarshalling -type piholeStatsResponsePrivate struct { - TotalQueries int `json:"dns_queries_today"` - QueriesSeries map[int64]int `json:"domains_over_time"` - BlockedQueries int `json:"ads_blocked_today"` - BlockedSeries map[int64]int `json:"ads_over_time"` - BlockedPercentage float64 `json:"ads_percentage_today"` - DomainsBlocked int `json:"domains_being_blocked"` +// Use custom unmarshal behavior to avoid not getting the rest of the valid data when unmarshalling +type piholeTopBlockedDomains map[string]int + +func (p *piholeTopBlockedDomains) UnmarshalJSON(data []byte) error { + // NOTE: do not change to piholeTopBlockedDomains type here or it will cause a stack overflow + // because of the UnmarshalJSON method getting called recursively + temp := make(map[string]int) + + err := json.Unmarshal(data, &temp) + + if err != nil { + *p = make(piholeTopBlockedDomains) + } else { + *p = temp + } + + return nil } func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { @@ -42,27 +52,13 @@ func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { if err != nil { return nil, err } - + responseJson, err := decodeJsonFromRequest[piholeStatsResponse](defaultClient, request) - + if err != nil { - // Refer to piholeStatsResponsePrivate above - responseJsonPriv, err := - decodeJsonFromRequest[piholeStatsResponsePrivate](defaultClient, request) - if err != nil { - return nil, err - } - - // Copy the results back to responseJson, leaving the TopBlockedDomains field empty - responseJson.TotalQueries = responseJsonPriv.TotalQueries - responseJson.QueriesSeries = responseJsonPriv.QueriesSeries - responseJson.BlockedQueries = responseJsonPriv.BlockedQueries - responseJson.BlockedSeries = responseJsonPriv.BlockedSeries - responseJson.BlockedPercentage = responseJsonPriv.BlockedPercentage - responseJson.TopBlockedDomains = make(map[string]int) - responseJson.DomainsBlocked = responseJsonPriv.DomainsBlocked + return nil, err } - + stats := &DNSStats{ TotalQueries: responseJson.TotalQueries, BlockedQueries: responseJson.BlockedQueries,