checker-blacklist/checker/disconnect.go
Pierre-Olivier Mercier c3cda1f104
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
Replace per-source enable booleans with SourcePrecheck and bump SDK to v1.9.0
Sources that always work (botvrij, disconnect, oisd, openphish, phishtank, quad9) drop their user-facing enable_* option; the rule's enabled/disabled state is now solely controlled by the SDK rule toggle. Sources that require credentials (criminalip, malwarebazaar, otx, pulsedive, safebrowsing, threatfox, urlhaus, virustotal) instead implement the new SourcePrecheck interface so the host UI can surface "not configured" before attempting a query.
2026-05-20 14:26:42 +08:00

149 lines
4.2 KiB
Go

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{}
}
func (s *disconnectSource) Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult {
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
}
}