Add Disconnect.me tracking-protection blocklist source
Downloads and caches the Disconnect.me services.json feed (24h TTL), matching domains against the Advertising, Analytics, Social, Content, and Disconnect categories. Severity is warn (privacy classification, not malware). Reuses the shared feedCache infrastructure.
This commit is contained in:
parent
9916ab0732
commit
c2cc88e1df
3 changed files with 290 additions and 0 deletions
162
checker/disconnect.go
Normal file
162
checker/disconnect.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const disconnectFeedURL = "https://s3.amazonaws.com/lists.disconnect.me/services.json"
|
||||
|
||||
func init() {
|
||||
Register(&disconnectSource{
|
||||
cache: newFeedCache(24*time.Hour, disconnectFetch(disconnectFeedURL)),
|
||||
})
|
||||
}
|
||||
|
||||
type disconnectSource struct{ cache *feedCache }
|
||||
|
||||
func (*disconnectSource) ID() string { return "disconnect" }
|
||||
func (*disconnectSource) Name() string { return "Disconnect.me" }
|
||||
|
||||
func (*disconnectSource) Options() SourceOptions {
|
||||
return SourceOptions{
|
||||
User: []sdk.CheckerOptionField{
|
||||
{
|
||||
Id: "enable_disconnect",
|
||||
Type: "bool",
|
||||
Label: "Use the Disconnect.me tracking-protection list",
|
||||
Description: "Check the domain against the Disconnect.me blocklist used by Firefox Enhanced Tracking Protection, Brave, and uBlock Origin.",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *disconnectSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
|
||||
if !sdk.GetBoolOption(opts, "enable_disconnect", true) {
|
||||
return disabledResult(s.ID(), s.Name())
|
||||
}
|
||||
if registered == "" {
|
||||
return []SourceResult{{SourceID: s.ID(), SourceName: s.Name(), Enabled: true}}
|
||||
}
|
||||
|
||||
matches, size, fetched, err := s.cache.lookup(ctx, registered)
|
||||
res := SourceResult{
|
||||
SourceID: s.ID(), SourceName: s.Name(), Enabled: true,
|
||||
Reference: "https://disconnect.me/trackerprotection",
|
||||
Details: mustJSON(map[string]any{"feed_size": size, "fetched_at": fetched}),
|
||||
}
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
|
||||
seenCategory := map[string]bool{}
|
||||
for _, m := range matches {
|
||||
parts := strings.SplitN(m, "|", 2)
|
||||
category, company := parts[0], ""
|
||||
if len(parts) == 2 {
|
||||
company = parts[1]
|
||||
}
|
||||
if !seenCategory[category] {
|
||||
seenCategory[category] = true
|
||||
res.Reasons = append(res.Reasons, category)
|
||||
}
|
||||
extra := map[string]string{}
|
||||
if company != "" {
|
||||
extra["company"] = company
|
||||
}
|
||||
res.Evidence = append(res.Evidence, Evidence{
|
||||
Label: "Category",
|
||||
Value: category,
|
||||
Status: strings.ToLower(category),
|
||||
Extra: extra,
|
||||
})
|
||||
}
|
||||
return []SourceResult{res}
|
||||
}
|
||||
|
||||
func (*disconnectSource) Evaluate(r SourceResult) (bool, string) {
|
||||
return evidenceEval(r, SeverityWarn)
|
||||
}
|
||||
|
||||
func (*disconnectSource) Diagnose(res SourceResult) Diagnosis {
|
||||
return Diagnosis{
|
||||
Severity: SeverityWarn,
|
||||
Title: "Listed in Disconnect.me tracking-protection blocklist",
|
||||
Detail: fmt.Sprintf(
|
||||
"Category: %s. This domain appears in the Disconnect.me list used by Firefox Enhanced Tracking Protection, Brave, and uBlock Origin. Browsers and privacy tools will block third-party requests to this domain, which may affect analytics, ad delivery, or embedded widgets. The list is maintained by Disconnect.me; contact them if you believe the classification is incorrect.",
|
||||
joinNonEmpty(res.Reasons, ", "),
|
||||
),
|
||||
Fix: "https://disconnect.me/contact",
|
||||
FixIsURL: true,
|
||||
}
|
||||
}
|
||||
|
||||
// disconnectJSON mirrors the structure of services.json.
|
||||
type disconnectJSON struct {
|
||||
Categories map[string][]map[string]map[string][]string `json:"categories"`
|
||||
}
|
||||
|
||||
func disconnectFetch(feedURL string) func(context.Context) ([]string, map[string][]string, error) {
|
||||
return func(ctx context.Context) ([]string, map[string][]string, error) {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, feedURL, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "happydomain-checker-blacklist/1.0")
|
||||
|
||||
resp, err := sharedHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("disconnect HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("disconnect read: %w", err)
|
||||
}
|
||||
|
||||
var data disconnectJSON
|
||||
if err := json.Unmarshal(raw, &data); err != nil {
|
||||
return nil, nil, fmt.Errorf("disconnect parse: %w", err)
|
||||
}
|
||||
|
||||
byHost := make(map[string][]string, 4096)
|
||||
for category, entities := range data.Categories {
|
||||
for _, entity := range entities {
|
||||
for company, sites := range entity {
|
||||
for _, domains := range sites {
|
||||
for _, d := range domains {
|
||||
d = strings.ToLower(strings.TrimSuffix(d, "."))
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
entry := category + "|" + company
|
||||
byHost[d] = append(byHost[d], entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
urls := make([]string, 0, len(byHost))
|
||||
for d := range byHost {
|
||||
urls = append(urls, d)
|
||||
}
|
||||
return urls, byHost, nil
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue