all: sync with master; upd chlog
This commit is contained in:
@@ -176,13 +176,16 @@ func (d *DNSFilter) filterExistsLocked(url string) (ok bool) {
|
||||
|
||||
// Add a filter
|
||||
// Return FALSE if a filter with this URL exists
|
||||
func (d *DNSFilter) filterAdd(flt FilterYAML) bool {
|
||||
func (d *DNSFilter) filterAdd(flt FilterYAML) (err error) {
|
||||
// Defer annotating to unlock sooner.
|
||||
defer func() { err = errors.Annotate(err, "adding filter: %w") }()
|
||||
|
||||
d.filtersMu.Lock()
|
||||
defer d.filtersMu.Unlock()
|
||||
|
||||
// Check for duplicates
|
||||
// Check for duplicates.
|
||||
if d.filterExistsLocked(flt.URL) {
|
||||
return false
|
||||
return errFilterExists
|
||||
}
|
||||
|
||||
if flt.white {
|
||||
@@ -190,7 +193,8 @@ func (d *DNSFilter) filterAdd(flt FilterYAML) bool {
|
||||
} else {
|
||||
d.Filters = append(d.Filters, flt)
|
||||
}
|
||||
return true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load filters from the disk
|
||||
@@ -238,6 +242,7 @@ func updateUniqueFilterID(filters []FilterYAML) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Improve this inexhaustible source of races.
|
||||
func assignUniqueFilterID() int64 {
|
||||
value := nextFilterID
|
||||
nextFilterID++
|
||||
@@ -343,29 +348,31 @@ func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int,
|
||||
}
|
||||
|
||||
updateCount := 0
|
||||
|
||||
d.filtersMu.Lock()
|
||||
defer d.filtersMu.Unlock()
|
||||
|
||||
for i := range updateFilters {
|
||||
uf := &updateFilters[i]
|
||||
updated := updateFlags[i]
|
||||
|
||||
d.filtersMu.Lock()
|
||||
for k := range *filters {
|
||||
f := &(*filters)[k]
|
||||
if f.ID != uf.ID || f.URL != uf.URL {
|
||||
continue
|
||||
}
|
||||
|
||||
f.LastUpdated = uf.LastUpdated
|
||||
if !updated {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("Updated filter #%d. Rules: %d -> %d",
|
||||
f.ID, f.RulesCount, uf.RulesCount)
|
||||
log.Info("Updated filter #%d. Rules: %d -> %d", f.ID, f.RulesCount, uf.RulesCount)
|
||||
f.Name = uf.Name
|
||||
f.RulesCount = uf.RulesCount
|
||||
f.checksum = uf.checksum
|
||||
updateCount++
|
||||
}
|
||||
d.filtersMu.Unlock()
|
||||
}
|
||||
|
||||
return updateCount, updateFilters, updateFlags, false
|
||||
@@ -421,11 +428,16 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
|
||||
if !updated {
|
||||
continue
|
||||
}
|
||||
_ = os.Remove(uf.Path(d.DataDir) + ".old")
|
||||
|
||||
p := uf.Path(d.DataDir)
|
||||
err := os.Remove(p + ".old")
|
||||
if err != nil {
|
||||
log.Debug("filtering: removing old filter file %q: %s", p, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("filtering: update finished")
|
||||
log.Debug("filtering: update finished: %d lists updated", updNum)
|
||||
|
||||
return updNum, false
|
||||
}
|
||||
@@ -467,8 +479,8 @@ func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err
|
||||
}
|
||||
|
||||
// parseFilter copies filter's content from src to dst and returns the number of
|
||||
// rules, name, number of bytes written, checksum, and title of the parsed list.
|
||||
// dst must not be nil.
|
||||
// rules, number of bytes written, checksum, and title of the parsed list. dst
|
||||
// must not be nil.
|
||||
func (d *DNSFilter) parseFilter(
|
||||
src io.Reader,
|
||||
dst io.Writer,
|
||||
@@ -550,14 +562,18 @@ func isHTML(line string) (ok bool) {
|
||||
return strings.HasPrefix(line, "<html") || strings.HasPrefix(line, "<!doctype")
|
||||
}
|
||||
|
||||
// Perform upgrade on a filter and update LastUpdated value
|
||||
func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
|
||||
b, err := d.updateIntl(filter)
|
||||
// update refreshes filter's content and a/mtimes of it's file.
|
||||
func (d *DNSFilter) update(filter *FilterYAML) (b bool, err error) {
|
||||
b, err = d.updateIntl(filter)
|
||||
filter.LastUpdated = time.Now()
|
||||
if !b {
|
||||
e := os.Chtimes(filter.Path(d.DataDir), filter.LastUpdated, filter.LastUpdated)
|
||||
if e != nil {
|
||||
log.Error("os.Chtimes(): %v", e)
|
||||
chErr := os.Chtimes(
|
||||
filter.Path(d.DataDir),
|
||||
filter.LastUpdated,
|
||||
filter.LastUpdated,
|
||||
)
|
||||
if chErr != nil {
|
||||
log.Error("os.Chtimes(): %v", chErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,11 +607,13 @@ func (d *DNSFilter) finalizeUpdate(
|
||||
return os.Remove(tmpFileName)
|
||||
}
|
||||
|
||||
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
|
||||
fltPath := flt.Path(d.DataDir)
|
||||
|
||||
log.Printf("saving contents of filter #%d into %s", flt.ID, fltPath)
|
||||
|
||||
// Don't use renamio or maybe packages, since those will require loading the
|
||||
// whole filter content to the memory on Windows.
|
||||
err = os.Rename(tmpFileName, flt.Path(d.DataDir))
|
||||
err = os.Rename(tmpFileName, fltPath)
|
||||
if err != nil {
|
||||
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
||||
}
|
||||
@@ -620,10 +638,14 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
||||
if ok && err == nil {
|
||||
finErr := d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs)
|
||||
if ok && finErr == nil {
|
||||
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.WithDeferred(err, finErr)
|
||||
}()
|
||||
|
||||
// Change the default 0o600 permission to something more acceptable by end
|
||||
@@ -634,7 +656,7 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
return false, fmt.Errorf("changing file mode: %w", err)
|
||||
}
|
||||
|
||||
var rc io.ReadCloser
|
||||
var r io.Reader
|
||||
if !filepath.IsAbs(flt.URL) {
|
||||
var resp *http.Response
|
||||
resp, err = d.HTTPClient.Get(flt.URL)
|
||||
@@ -651,16 +673,19 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
rc = resp.Body
|
||||
r = resp.Body
|
||||
} else {
|
||||
rc, err = os.Open(flt.URL)
|
||||
var f *os.File
|
||||
f, err = os.Open(flt.URL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, rc.Close()) }()
|
||||
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||
|
||||
r = f
|
||||
}
|
||||
|
||||
rnum, n, cs, name, err = d.parseFilter(rc, tmpFile)
|
||||
rnum, n, cs, name, err = d.parseFilter(r, tmpFile)
|
||||
|
||||
return cs != flt.checksum && err == nil, err
|
||||
}
|
||||
@@ -705,10 +730,11 @@ func (d *DNSFilter) EnableFilters(async bool) {
|
||||
}
|
||||
|
||||
func (d *DNSFilter) enableFiltersLocked(async bool) {
|
||||
filters := []Filter{{
|
||||
filters := make([]Filter, 1, len(d.Filters)+len(d.WhitelistFilters)+1)
|
||||
filters[0] = Filter{
|
||||
ID: CustomListID,
|
||||
Data: []byte(strings.Join(d.UserRules, "\n")),
|
||||
}}
|
||||
}
|
||||
|
||||
for _, filter := range d.Filters {
|
||||
if !filter.Enabled {
|
||||
|
||||
@@ -63,6 +63,9 @@ type Settings struct {
|
||||
SafeSearchEnabled bool
|
||||
SafeBrowsingEnabled bool
|
||||
ParentalEnabled bool
|
||||
|
||||
// ClientSafeSearch is a client configured safe search.
|
||||
ClientSafeSearch SafeSearch
|
||||
}
|
||||
|
||||
// Resolver is the interface for net.Resolver to simplify testing.
|
||||
@@ -83,13 +86,16 @@ type Config struct {
|
||||
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
|
||||
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
|
||||
SafeBrowsingCacheSize uint `yaml:"safebrowsing_cache_size"` // (in bytes)
|
||||
SafeSearchCacheSize uint `yaml:"safesearch_cache_size"` // (in bytes)
|
||||
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
|
||||
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)
|
||||
// TODO(a.garipov): Use timeutil.Duration
|
||||
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)
|
||||
|
||||
SafeSearchConf SafeSearchConfig `yaml:"safe_search"`
|
||||
SafeSearch SafeSearch `yaml:"-"`
|
||||
|
||||
Rewrites []*LegacyRewrite `yaml:"rewrites"`
|
||||
|
||||
@@ -107,9 +113,6 @@ type Config struct {
|
||||
// Register an HTTP handler
|
||||
HTTPRegister aghhttp.RegisterFunc `yaml:"-"`
|
||||
|
||||
// CustomResolver is the resolver used by DNSFilter.
|
||||
CustomResolver Resolver `yaml:"-"`
|
||||
|
||||
// HTTPClient is the client to use for updating the remote filters.
|
||||
HTTPClient *http.Client `yaml:"-"`
|
||||
|
||||
@@ -172,7 +175,6 @@ type DNSFilter struct {
|
||||
|
||||
safebrowsingCache cache.Cache
|
||||
parentalCache cache.Cache
|
||||
safeSearchCache cache.Cache
|
||||
|
||||
Config // for direct access by library users, even a = assignment
|
||||
// confLock protects Config.
|
||||
@@ -182,11 +184,6 @@ type DNSFilter struct {
|
||||
filtersInitializerChan chan filtersInitializerParams
|
||||
filtersInitializerLock sync.Mutex
|
||||
|
||||
// resolver only looks up the IP address of the host while safe search.
|
||||
//
|
||||
// TODO(e.burkov): Use upstream that configured in dnsforward instead.
|
||||
resolver Resolver
|
||||
|
||||
refreshLock *sync.Mutex
|
||||
|
||||
// filterTitleRegexp is the regular expression to retrieve a name of a
|
||||
@@ -195,6 +192,7 @@ type DNSFilter struct {
|
||||
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
|
||||
filterTitleRegexp *regexp.Regexp
|
||||
|
||||
safeSearch SafeSearch
|
||||
hostCheckers []hostChecker
|
||||
}
|
||||
|
||||
@@ -298,7 +296,7 @@ func (d *DNSFilter) GetConfig() (s Settings) {
|
||||
|
||||
return Settings{
|
||||
FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0,
|
||||
SafeSearchEnabled: d.Config.SafeSearchEnabled,
|
||||
SafeSearchEnabled: d.Config.SafeSearchConf.Enabled,
|
||||
SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled,
|
||||
ParentalEnabled: d.Config.ParentalEnabled,
|
||||
}
|
||||
@@ -942,7 +940,6 @@ func InitModule() {
|
||||
// be non-nil.
|
||||
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
||||
d = &DNSFilter{
|
||||
resolver: net.DefaultResolver,
|
||||
refreshLock: &sync.Mutex{},
|
||||
filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`),
|
||||
}
|
||||
@@ -951,18 +948,12 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
||||
EnableLRU: true,
|
||||
MaxSize: c.SafeBrowsingCacheSize,
|
||||
})
|
||||
d.safeSearchCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.SafeSearchCacheSize,
|
||||
})
|
||||
d.parentalCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.ParentalCacheSize,
|
||||
})
|
||||
|
||||
if r := c.CustomResolver; r != nil {
|
||||
d.resolver = r
|
||||
}
|
||||
d.safeSearch = c.SafeSearch
|
||||
|
||||
d.hostCheckers = []hostChecker{{
|
||||
check: d.matchSysHosts,
|
||||
|
||||
@@ -2,10 +2,8 @@ package filtering
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
@@ -33,7 +31,6 @@ func purgeCaches(d *DNSFilter) {
|
||||
for _, c := range []cache.Cache{
|
||||
d.safebrowsingCache,
|
||||
d.parentalCache,
|
||||
d.safeSearchCache,
|
||||
} {
|
||||
if c != nil {
|
||||
c.Clear()
|
||||
@@ -51,7 +48,7 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
|
||||
c.ParentalCacheSize = 10000
|
||||
c.SafeSearchCacheSize = 1000
|
||||
c.CacheTime = 30
|
||||
setts.SafeSearchEnabled = c.SafeSearchEnabled
|
||||
setts.SafeSearchEnabled = c.SafeSearchConf.Enabled
|
||||
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
|
||||
setts.ParentalEnabled = c.ParentalEnabled
|
||||
} else {
|
||||
@@ -216,164 +213,6 @@ func TestParallelSB(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Safe Search.
|
||||
|
||||
func TestSafeSearch(t *testing.T) {
|
||||
d, _ := newForTest(t, &Config{SafeSearchEnabled: true}, nil)
|
||||
t.Cleanup(d.Close)
|
||||
val, ok := d.SafeSearchDomain("www.google.com")
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, "forcesafesearch.google.com", val)
|
||||
}
|
||||
|
||||
func TestCheckHostSafeSearchYandex(t *testing.T) {
|
||||
d, setts := newForTest(t, &Config{
|
||||
SafeSearchEnabled: true,
|
||||
}, nil)
|
||||
t.Cleanup(d.Close)
|
||||
|
||||
yandexIP := net.IPv4(213, 180, 193, 56)
|
||||
|
||||
// Check host for each domain.
|
||||
for _, host := range []string{
|
||||
"yAndeX.ru",
|
||||
"YANdex.COM",
|
||||
"yandex.ua",
|
||||
"yandex.by",
|
||||
"yandex.kz",
|
||||
"www.yandex.com",
|
||||
} {
|
||||
t.Run(strings.ToLower(host), func(t *testing.T) {
|
||||
res, err := d.CheckHost(host, dns.TypeA, setts)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, yandexIP, res.Rules[0].IP)
|
||||
assert.EqualValues(t, SafeSearchListID, res.Rules[0].FilterListID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
||||
resolver := &aghtest.TestResolver{}
|
||||
d, setts := newForTest(t, &Config{
|
||||
SafeSearchEnabled: true,
|
||||
CustomResolver: resolver,
|
||||
}, nil)
|
||||
t.Cleanup(d.Close)
|
||||
|
||||
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
||||
|
||||
// Check host for each domain.
|
||||
for _, host := range []string{
|
||||
"www.google.com",
|
||||
"www.google.im",
|
||||
"www.google.co.in",
|
||||
"www.google.iq",
|
||||
"www.google.is",
|
||||
"www.google.it",
|
||||
"www.google.je",
|
||||
} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
res, err := d.CheckHost(host, dns.TypeA, setts)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, ip, res.Rules[0].IP)
|
||||
assert.EqualValues(t, SafeSearchListID, res.Rules[0].FilterListID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheYandex(t *testing.T) {
|
||||
d, setts := newForTest(t, nil, nil)
|
||||
t.Cleanup(d.Close)
|
||||
const domain = "yandex.ru"
|
||||
|
||||
// Check host with disabled safesearch.
|
||||
res, err := d.CheckHost(domain, dns.TypeA, setts)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
|
||||
require.Empty(t, res.Rules)
|
||||
|
||||
yandexIP := net.IPv4(213, 180, 193, 56)
|
||||
|
||||
d, setts = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
|
||||
t.Cleanup(d.Close)
|
||||
|
||||
res, err = d.CheckHost(domain, dns.TypeA, setts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// For yandex we already know valid IP.
|
||||
require.Len(t, res.Rules, 1)
|
||||
assert.Equal(t, res.Rules[0].IP, yandexIP)
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := getCachedResult(d.safeSearchCache, domain)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||
resolver := &aghtest.TestResolver{}
|
||||
d, setts := newForTest(t, &Config{
|
||||
CustomResolver: resolver,
|
||||
}, nil)
|
||||
t.Cleanup(d.Close)
|
||||
|
||||
const domain = "www.google.ru"
|
||||
res, err := d.CheckHost(domain, dns.TypeA, setts)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
|
||||
require.Empty(t, res.Rules)
|
||||
|
||||
d, setts = newForTest(t, &Config{SafeSearchEnabled: true}, nil)
|
||||
t.Cleanup(d.Close)
|
||||
d.resolver = resolver
|
||||
|
||||
// Lookup for safesearch domain.
|
||||
safeDomain, ok := d.SafeSearchDomain(domain)
|
||||
require.True(t, ok)
|
||||
|
||||
ips, err := resolver.LookupIP(context.Background(), "ip", safeDomain)
|
||||
require.NoError(t, err)
|
||||
|
||||
var ip net.IP
|
||||
for _, foundIP := range ips {
|
||||
if foundIP.To4() != nil {
|
||||
ip = foundIP
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res, err = d.CheckHost(domain, dns.TypeA, setts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.True(t, res.Rules[0].IP.Equal(ip))
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := getCachedResult(d.safeSearchCache, domain)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.True(t, cachedValue.Rules[0].IP.Equal(ip))
|
||||
}
|
||||
|
||||
// Parental.
|
||||
|
||||
func TestParentalControl(t *testing.T) {
|
||||
@@ -854,27 +693,3 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSafeSearch(b *testing.B) {
|
||||
d, _ := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
|
||||
b.Cleanup(d.Close)
|
||||
for n := 0; n < b.N; n++ {
|
||||
val, ok := d.SafeSearchDomain("www.google.com")
|
||||
require.True(b, ok)
|
||||
|
||||
assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSafeSearchParallel(b *testing.B) {
|
||||
d, _ := newForTest(b, &Config{SafeSearchEnabled: true}, nil)
|
||||
b.Cleanup(d.Close)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
val, ok := d.SafeSearchDomain("www.google.com")
|
||||
require.True(b, ok)
|
||||
|
||||
assert.Equal(b, "forcesafesearch.google.com", val, "Expected safesearch for google.com to be forcesafesearch.google.com")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,26 +14,33 @@ import (
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// validateFilterURL validates the filter list URL or file name.
|
||||
func validateFilterURL(urlStr string) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "checking filter: %w") }()
|
||||
|
||||
if filepath.IsAbs(urlStr) {
|
||||
_, err = os.Stat(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking filter file: %w", err)
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
url, err := url.ParseRequestURI(urlStr)
|
||||
u, err := url.ParseRequestURI(urlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking filter url: %w", err)
|
||||
}
|
||||
|
||||
if s := url.Scheme; s != aghhttp.SchemeHTTP && s != aghhttp.SchemeHTTPS {
|
||||
return fmt.Errorf("checking filter url: invalid scheme %q", s)
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
} else if s := u.Scheme; s != aghhttp.SchemeHTTP && s != aghhttp.SchemeHTTPS {
|
||||
return &url.Error{
|
||||
Op: "Check scheme",
|
||||
URL: urlStr,
|
||||
Err: fmt.Errorf("only %v allowed", []string{aghhttp.SchemeHTTP, aghhttp.SchemeHTTPS}),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -63,7 +70,8 @@ func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Check for duplicates
|
||||
if d.filterExists(fj.URL) {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
|
||||
err = errFilterExists
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Filter with URL %q: %s", fj.URL, err)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -99,7 +107,7 @@ func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
r,
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
"Filter at the url %s is invalid (maybe it points to blank page?)",
|
||||
"Filter with URL %q is invalid (maybe it points to blank page?)",
|
||||
filt.URL,
|
||||
)
|
||||
|
||||
@@ -108,8 +116,9 @@ func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// URL is assumed valid so append it to filters, update config, write new
|
||||
// file and reload it to engines.
|
||||
if !d.filterAdd(filt) {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", filt.URL)
|
||||
err = d.filterAdd(filt)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Filter with URL %q: %s", filt.URL, err)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -137,31 +146,38 @@ func (d *DNSFilter) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
d.filtersMu.Lock()
|
||||
filters := &d.Filters
|
||||
if req.Whitelist {
|
||||
filters = &d.WhitelistFilters
|
||||
}
|
||||
|
||||
var deleted FilterYAML
|
||||
var newFilters []FilterYAML
|
||||
for _, flt := range *filters {
|
||||
if flt.URL != req.URL {
|
||||
newFilters = append(newFilters, flt)
|
||||
func() {
|
||||
d.filtersMu.Lock()
|
||||
defer d.filtersMu.Unlock()
|
||||
|
||||
continue
|
||||
filters := &d.Filters
|
||||
if req.Whitelist {
|
||||
filters = &d.WhitelistFilters
|
||||
}
|
||||
|
||||
deleted = flt
|
||||
path := flt.Path(d.DataDir)
|
||||
err = os.Rename(path, path+".old")
|
||||
delIdx := slices.IndexFunc(*filters, func(flt FilterYAML) bool {
|
||||
return flt.URL == req.URL
|
||||
})
|
||||
if delIdx == -1 {
|
||||
log.Error("deleting filter with url %q: %s", req.URL, errFilterNotExist)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
deleted = (*filters)[delIdx]
|
||||
p := deleted.Path(d.DataDir)
|
||||
err = os.Rename(p, p+".old")
|
||||
if err != nil {
|
||||
log.Error("deleting filter %q: %s", path, err)
|
||||
}
|
||||
}
|
||||
log.Error("deleting filter %d: renaming file %q: %s", deleted.ID, p, err)
|
||||
|
||||
*filters = newFilters
|
||||
d.filtersMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
*filters = slices.Delete(*filters, delIdx, delIdx+1)
|
||||
|
||||
log.Info("deleted filter %d", deleted.ID)
|
||||
}()
|
||||
|
||||
d.ConfigModified()
|
||||
d.EnableFilters(true)
|
||||
@@ -258,10 +274,6 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
|
||||
type Req struct {
|
||||
White bool `json:"whitelist"`
|
||||
}
|
||||
type Resp struct {
|
||||
Updated int `json:"updated"`
|
||||
}
|
||||
resp := Resp{}
|
||||
var err error
|
||||
|
||||
req := Req{}
|
||||
@@ -273,6 +285,9 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
var ok bool
|
||||
resp := struct {
|
||||
Updated int `json:"updated"`
|
||||
}{}
|
||||
resp.Updated, _, ok = d.tryRefreshFilters(!req.White, req.White, true)
|
||||
if !ok {
|
||||
aghhttp.Error(
|
||||
@@ -461,6 +476,7 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
|
||||
registerHTTP(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable)
|
||||
registerHTTP(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable)
|
||||
registerHTTP(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus)
|
||||
registerHTTP(http.MethodPut, "/control/safesearch/settings", d.handleSafeSearchSettings)
|
||||
|
||||
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
|
||||
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
package filtering
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
)
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// SafeSearch interface describes a service for search engines hosts rewrites.
|
||||
type SafeSearch interface {
|
||||
// SearchHost returns a replacement address for the search engine host.
|
||||
SearchHost(host string, qtype uint16) (res *rules.DNSRewrite)
|
||||
|
||||
// CheckHost checks host with safe search engine.
|
||||
// CheckHost checks host with safe search filter. CheckHost must be safe
|
||||
// for concurrent use. qtype must be either [dns.TypeA] or [dns.TypeAAAA].
|
||||
CheckHost(host string, qtype uint16) (res Result, err error)
|
||||
|
||||
// Update updates the configuration of the safe search filter. Update must
|
||||
// be safe for concurrent use. An implementation of Update may ignore some
|
||||
// fields, but it must document which.
|
||||
Update(conf SafeSearchConfig) (err error)
|
||||
}
|
||||
|
||||
// SafeSearchConfig is a struct with safe search related settings.
|
||||
type SafeSearchConfig struct {
|
||||
// CustomResolver is the resolver used by safe search.
|
||||
CustomResolver Resolver `yaml:"-"`
|
||||
CustomResolver Resolver `yaml:"-" json:"-"`
|
||||
|
||||
// Enabled indicates if safe search is enabled entirely.
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
@@ -44,358 +33,27 @@ type SafeSearchConfig struct {
|
||||
YouTube bool `yaml:"youtube" json:"youtube"`
|
||||
}
|
||||
|
||||
/*
|
||||
expire byte[4]
|
||||
res Result
|
||||
*/
|
||||
func (d *DNSFilter) setCacheResult(cache cache.Cache, host string, res Result) int {
|
||||
var buf bytes.Buffer
|
||||
|
||||
expire := uint(time.Now().Unix()) + d.Config.CacheTime*60
|
||||
exp := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(exp, uint32(expire))
|
||||
_, _ = buf.Write(exp)
|
||||
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(res)
|
||||
if err != nil {
|
||||
log.Error("gob.Encode(): %s", err)
|
||||
return 0
|
||||
}
|
||||
val := buf.Bytes()
|
||||
_ = cache.Set([]byte(host), val)
|
||||
return len(val)
|
||||
}
|
||||
|
||||
func getCachedResult(cache cache.Cache, host string) (Result, bool) {
|
||||
data := cache.Get([]byte(host))
|
||||
if data == nil {
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
exp := int(binary.BigEndian.Uint32(data[:4]))
|
||||
if exp <= int(time.Now().Unix()) {
|
||||
cache.Del([]byte(host))
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.Write(data[4:])
|
||||
dec := gob.NewDecoder(&buf)
|
||||
r := Result{}
|
||||
err := dec.Decode(&r)
|
||||
if err != nil {
|
||||
log.Debug("gob.Decode(): %s", err)
|
||||
return Result{}, false
|
||||
}
|
||||
|
||||
return r, true
|
||||
}
|
||||
|
||||
// SafeSearchDomain returns replacement address for search engine
|
||||
func (d *DNSFilter) SafeSearchDomain(host string) (string, bool) {
|
||||
val, ok := safeSearchDomains[host]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// checkSafeSearch checks host with safe search engine. Matches
|
||||
// [hostChecker.check].
|
||||
func (d *DNSFilter) checkSafeSearch(
|
||||
host string,
|
||||
_ uint16,
|
||||
qtype uint16,
|
||||
setts *Settings,
|
||||
) (res Result, err error) {
|
||||
if !setts.ProtectionEnabled || !setts.SafeSearchEnabled {
|
||||
if !setts.ProtectionEnabled ||
|
||||
!setts.SafeSearchEnabled ||
|
||||
(qtype != dns.TypeA && qtype != dns.TypeAAAA) {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("SafeSearch: lookup for %s", host)
|
||||
}
|
||||
|
||||
// Check cache. Return cached result if it was found
|
||||
cachedValue, isFound := getCachedResult(d.safeSearchCache, host)
|
||||
if isFound {
|
||||
// atomic.AddUint64(&gctx.stats.Safesearch.CacheHits, 1)
|
||||
log.Tracef("SafeSearch: found in cache: %s", host)
|
||||
return cachedValue, nil
|
||||
}
|
||||
|
||||
safeHost, ok := d.SafeSearchDomain(host)
|
||||
if !ok {
|
||||
if d.safeSearch == nil {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
res = Result{
|
||||
Rules: []*ResultRule{{
|
||||
FilterListID: SafeSearchListID,
|
||||
}},
|
||||
Reason: FilteredSafeSearch,
|
||||
IsFiltered: true,
|
||||
clientSafeSearch := setts.ClientSafeSearch
|
||||
if clientSafeSearch != nil {
|
||||
return clientSafeSearch.CheckHost(host, qtype)
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(safeHost); ip != nil {
|
||||
res.Rules[0].IP = ip
|
||||
valLen := d.setCacheResult(d.safeSearchCache, host, res)
|
||||
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, valLen)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ips, err := d.resolver.LookupIP(context.Background(), "ip", safeHost)
|
||||
if err != nil {
|
||||
log.Tracef("SafeSearchDomain for %s was found but failed to lookup for %s cause %s", host, safeHost, err)
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip = ip.To4(); ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
res.Rules[0].IP = ip
|
||||
|
||||
l := d.setCacheResult(d.safeSearchCache, host, res)
|
||||
log.Debug("SafeSearch: stored in cache: %s (%d bytes)", host, l)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", safeHost)
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
|
||||
setProtectedBool(&d.confLock, &d.Config.SafeSearchEnabled, true)
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
|
||||
setProtectedBool(&d.confLock, &d.Config.SafeSearchEnabled, false)
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
||||
resp := &struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}{
|
||||
Enabled: protectedBool(&d.confLock, &d.Config.SafeSearchEnabled),
|
||||
}
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
var safeSearchDomains = map[string]string{
|
||||
"yandex.com": "213.180.193.56",
|
||||
"yandex.ru": "213.180.193.56",
|
||||
"yandex.ua": "213.180.193.56",
|
||||
"yandex.by": "213.180.193.56",
|
||||
"yandex.kz": "213.180.193.56",
|
||||
"www.yandex.com": "213.180.193.56",
|
||||
"www.yandex.ru": "213.180.193.56",
|
||||
"www.yandex.ua": "213.180.193.56",
|
||||
"www.yandex.by": "213.180.193.56",
|
||||
"www.yandex.kz": "213.180.193.56",
|
||||
|
||||
"www.bing.com": "strict.bing.com",
|
||||
|
||||
"duckduckgo.com": "safe.duckduckgo.com",
|
||||
"www.duckduckgo.com": "safe.duckduckgo.com",
|
||||
"start.duckduckgo.com": "safe.duckduckgo.com",
|
||||
|
||||
"www.google.com": "forcesafesearch.google.com",
|
||||
"www.google.ad": "forcesafesearch.google.com",
|
||||
"www.google.ae": "forcesafesearch.google.com",
|
||||
"www.google.com.af": "forcesafesearch.google.com",
|
||||
"www.google.com.ag": "forcesafesearch.google.com",
|
||||
"www.google.com.ai": "forcesafesearch.google.com",
|
||||
"www.google.al": "forcesafesearch.google.com",
|
||||
"www.google.am": "forcesafesearch.google.com",
|
||||
"www.google.co.ao": "forcesafesearch.google.com",
|
||||
"www.google.com.ar": "forcesafesearch.google.com",
|
||||
"www.google.as": "forcesafesearch.google.com",
|
||||
"www.google.at": "forcesafesearch.google.com",
|
||||
"www.google.com.au": "forcesafesearch.google.com",
|
||||
"www.google.az": "forcesafesearch.google.com",
|
||||
"www.google.ba": "forcesafesearch.google.com",
|
||||
"www.google.com.bd": "forcesafesearch.google.com",
|
||||
"www.google.be": "forcesafesearch.google.com",
|
||||
"www.google.bf": "forcesafesearch.google.com",
|
||||
"www.google.bg": "forcesafesearch.google.com",
|
||||
"www.google.com.bh": "forcesafesearch.google.com",
|
||||
"www.google.bi": "forcesafesearch.google.com",
|
||||
"www.google.bj": "forcesafesearch.google.com",
|
||||
"www.google.com.bn": "forcesafesearch.google.com",
|
||||
"www.google.com.bo": "forcesafesearch.google.com",
|
||||
"www.google.com.br": "forcesafesearch.google.com",
|
||||
"www.google.bs": "forcesafesearch.google.com",
|
||||
"www.google.bt": "forcesafesearch.google.com",
|
||||
"www.google.co.bw": "forcesafesearch.google.com",
|
||||
"www.google.by": "forcesafesearch.google.com",
|
||||
"www.google.com.bz": "forcesafesearch.google.com",
|
||||
"www.google.ca": "forcesafesearch.google.com",
|
||||
"www.google.cd": "forcesafesearch.google.com",
|
||||
"www.google.cf": "forcesafesearch.google.com",
|
||||
"www.google.cg": "forcesafesearch.google.com",
|
||||
"www.google.ch": "forcesafesearch.google.com",
|
||||
"www.google.ci": "forcesafesearch.google.com",
|
||||
"www.google.co.ck": "forcesafesearch.google.com",
|
||||
"www.google.cl": "forcesafesearch.google.com",
|
||||
"www.google.cm": "forcesafesearch.google.com",
|
||||
"www.google.cn": "forcesafesearch.google.com",
|
||||
"www.google.com.co": "forcesafesearch.google.com",
|
||||
"www.google.co.cr": "forcesafesearch.google.com",
|
||||
"www.google.com.cu": "forcesafesearch.google.com",
|
||||
"www.google.cv": "forcesafesearch.google.com",
|
||||
"www.google.com.cy": "forcesafesearch.google.com",
|
||||
"www.google.cz": "forcesafesearch.google.com",
|
||||
"www.google.de": "forcesafesearch.google.com",
|
||||
"www.google.dj": "forcesafesearch.google.com",
|
||||
"www.google.dk": "forcesafesearch.google.com",
|
||||
"www.google.dm": "forcesafesearch.google.com",
|
||||
"www.google.com.do": "forcesafesearch.google.com",
|
||||
"www.google.dz": "forcesafesearch.google.com",
|
||||
"www.google.com.ec": "forcesafesearch.google.com",
|
||||
"www.google.ee": "forcesafesearch.google.com",
|
||||
"www.google.com.eg": "forcesafesearch.google.com",
|
||||
"www.google.es": "forcesafesearch.google.com",
|
||||
"www.google.com.et": "forcesafesearch.google.com",
|
||||
"www.google.fi": "forcesafesearch.google.com",
|
||||
"www.google.com.fj": "forcesafesearch.google.com",
|
||||
"www.google.fm": "forcesafesearch.google.com",
|
||||
"www.google.fr": "forcesafesearch.google.com",
|
||||
"www.google.ga": "forcesafesearch.google.com",
|
||||
"www.google.ge": "forcesafesearch.google.com",
|
||||
"www.google.gg": "forcesafesearch.google.com",
|
||||
"www.google.com.gh": "forcesafesearch.google.com",
|
||||
"www.google.com.gi": "forcesafesearch.google.com",
|
||||
"www.google.gl": "forcesafesearch.google.com",
|
||||
"www.google.gm": "forcesafesearch.google.com",
|
||||
"www.google.gp": "forcesafesearch.google.com",
|
||||
"www.google.gr": "forcesafesearch.google.com",
|
||||
"www.google.com.gt": "forcesafesearch.google.com",
|
||||
"www.google.gy": "forcesafesearch.google.com",
|
||||
"www.google.com.hk": "forcesafesearch.google.com",
|
||||
"www.google.hn": "forcesafesearch.google.com",
|
||||
"www.google.hr": "forcesafesearch.google.com",
|
||||
"www.google.ht": "forcesafesearch.google.com",
|
||||
"www.google.hu": "forcesafesearch.google.com",
|
||||
"www.google.co.id": "forcesafesearch.google.com",
|
||||
"www.google.ie": "forcesafesearch.google.com",
|
||||
"www.google.co.il": "forcesafesearch.google.com",
|
||||
"www.google.im": "forcesafesearch.google.com",
|
||||
"www.google.co.in": "forcesafesearch.google.com",
|
||||
"www.google.iq": "forcesafesearch.google.com",
|
||||
"www.google.is": "forcesafesearch.google.com",
|
||||
"www.google.it": "forcesafesearch.google.com",
|
||||
"www.google.je": "forcesafesearch.google.com",
|
||||
"www.google.com.jm": "forcesafesearch.google.com",
|
||||
"www.google.jo": "forcesafesearch.google.com",
|
||||
"www.google.co.jp": "forcesafesearch.google.com",
|
||||
"www.google.co.ke": "forcesafesearch.google.com",
|
||||
"www.google.com.kh": "forcesafesearch.google.com",
|
||||
"www.google.ki": "forcesafesearch.google.com",
|
||||
"www.google.kg": "forcesafesearch.google.com",
|
||||
"www.google.co.kr": "forcesafesearch.google.com",
|
||||
"www.google.com.kw": "forcesafesearch.google.com",
|
||||
"www.google.kz": "forcesafesearch.google.com",
|
||||
"www.google.la": "forcesafesearch.google.com",
|
||||
"www.google.com.lb": "forcesafesearch.google.com",
|
||||
"www.google.li": "forcesafesearch.google.com",
|
||||
"www.google.lk": "forcesafesearch.google.com",
|
||||
"www.google.co.ls": "forcesafesearch.google.com",
|
||||
"www.google.lt": "forcesafesearch.google.com",
|
||||
"www.google.lu": "forcesafesearch.google.com",
|
||||
"www.google.lv": "forcesafesearch.google.com",
|
||||
"www.google.com.ly": "forcesafesearch.google.com",
|
||||
"www.google.co.ma": "forcesafesearch.google.com",
|
||||
"www.google.md": "forcesafesearch.google.com",
|
||||
"www.google.me": "forcesafesearch.google.com",
|
||||
"www.google.mg": "forcesafesearch.google.com",
|
||||
"www.google.mk": "forcesafesearch.google.com",
|
||||
"www.google.ml": "forcesafesearch.google.com",
|
||||
"www.google.com.mm": "forcesafesearch.google.com",
|
||||
"www.google.mn": "forcesafesearch.google.com",
|
||||
"www.google.ms": "forcesafesearch.google.com",
|
||||
"www.google.com.mt": "forcesafesearch.google.com",
|
||||
"www.google.mu": "forcesafesearch.google.com",
|
||||
"www.google.mv": "forcesafesearch.google.com",
|
||||
"www.google.mw": "forcesafesearch.google.com",
|
||||
"www.google.com.mx": "forcesafesearch.google.com",
|
||||
"www.google.com.my": "forcesafesearch.google.com",
|
||||
"www.google.co.mz": "forcesafesearch.google.com",
|
||||
"www.google.com.na": "forcesafesearch.google.com",
|
||||
"www.google.com.nf": "forcesafesearch.google.com",
|
||||
"www.google.com.ng": "forcesafesearch.google.com",
|
||||
"www.google.com.ni": "forcesafesearch.google.com",
|
||||
"www.google.ne": "forcesafesearch.google.com",
|
||||
"www.google.nl": "forcesafesearch.google.com",
|
||||
"www.google.no": "forcesafesearch.google.com",
|
||||
"www.google.com.np": "forcesafesearch.google.com",
|
||||
"www.google.nr": "forcesafesearch.google.com",
|
||||
"www.google.nu": "forcesafesearch.google.com",
|
||||
"www.google.co.nz": "forcesafesearch.google.com",
|
||||
"www.google.com.om": "forcesafesearch.google.com",
|
||||
"www.google.com.pa": "forcesafesearch.google.com",
|
||||
"www.google.com.pe": "forcesafesearch.google.com",
|
||||
"www.google.com.pg": "forcesafesearch.google.com",
|
||||
"www.google.com.ph": "forcesafesearch.google.com",
|
||||
"www.google.com.pk": "forcesafesearch.google.com",
|
||||
"www.google.pl": "forcesafesearch.google.com",
|
||||
"www.google.pn": "forcesafesearch.google.com",
|
||||
"www.google.com.pr": "forcesafesearch.google.com",
|
||||
"www.google.ps": "forcesafesearch.google.com",
|
||||
"www.google.pt": "forcesafesearch.google.com",
|
||||
"www.google.com.py": "forcesafesearch.google.com",
|
||||
"www.google.com.qa": "forcesafesearch.google.com",
|
||||
"www.google.ro": "forcesafesearch.google.com",
|
||||
"www.google.ru": "forcesafesearch.google.com",
|
||||
"www.google.rw": "forcesafesearch.google.com",
|
||||
"www.google.com.sa": "forcesafesearch.google.com",
|
||||
"www.google.com.sb": "forcesafesearch.google.com",
|
||||
"www.google.sc": "forcesafesearch.google.com",
|
||||
"www.google.se": "forcesafesearch.google.com",
|
||||
"www.google.com.sg": "forcesafesearch.google.com",
|
||||
"www.google.sh": "forcesafesearch.google.com",
|
||||
"www.google.si": "forcesafesearch.google.com",
|
||||
"www.google.sk": "forcesafesearch.google.com",
|
||||
"www.google.com.sl": "forcesafesearch.google.com",
|
||||
"www.google.sn": "forcesafesearch.google.com",
|
||||
"www.google.so": "forcesafesearch.google.com",
|
||||
"www.google.sm": "forcesafesearch.google.com",
|
||||
"www.google.sr": "forcesafesearch.google.com",
|
||||
"www.google.st": "forcesafesearch.google.com",
|
||||
"www.google.com.sv": "forcesafesearch.google.com",
|
||||
"www.google.td": "forcesafesearch.google.com",
|
||||
"www.google.tg": "forcesafesearch.google.com",
|
||||
"www.google.co.th": "forcesafesearch.google.com",
|
||||
"www.google.com.tj": "forcesafesearch.google.com",
|
||||
"www.google.tk": "forcesafesearch.google.com",
|
||||
"www.google.tl": "forcesafesearch.google.com",
|
||||
"www.google.tm": "forcesafesearch.google.com",
|
||||
"www.google.tn": "forcesafesearch.google.com",
|
||||
"www.google.to": "forcesafesearch.google.com",
|
||||
"www.google.com.tr": "forcesafesearch.google.com",
|
||||
"www.google.tt": "forcesafesearch.google.com",
|
||||
"www.google.com.tw": "forcesafesearch.google.com",
|
||||
"www.google.co.tz": "forcesafesearch.google.com",
|
||||
"www.google.com.ua": "forcesafesearch.google.com",
|
||||
"www.google.co.ug": "forcesafesearch.google.com",
|
||||
"www.google.co.uk": "forcesafesearch.google.com",
|
||||
"www.google.com.uy": "forcesafesearch.google.com",
|
||||
"www.google.co.uz": "forcesafesearch.google.com",
|
||||
"www.google.com.vc": "forcesafesearch.google.com",
|
||||
"www.google.co.ve": "forcesafesearch.google.com",
|
||||
"www.google.vg": "forcesafesearch.google.com",
|
||||
"www.google.co.vi": "forcesafesearch.google.com",
|
||||
"www.google.com.vn": "forcesafesearch.google.com",
|
||||
"www.google.vu": "forcesafesearch.google.com",
|
||||
"www.google.ws": "forcesafesearch.google.com",
|
||||
"www.google.rs": "forcesafesearch.google.com",
|
||||
|
||||
"www.youtube.com": "restrictmoderate.youtube.com",
|
||||
"m.youtube.com": "restrictmoderate.youtube.com",
|
||||
"youtubei.googleapis.com": "restrictmoderate.youtube.com",
|
||||
"youtube.googleapis.com": "restrictmoderate.youtube.com",
|
||||
"www.youtube-nocookie.com": "restrictmoderate.youtube.com",
|
||||
|
||||
"pixabay.com": "safesearch.pixabay.com",
|
||||
return d.safeSearch.CheckHost(host, qtype)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
|duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||
|start.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||
|www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||
|www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||
|
||||
@@ -188,4 +188,4 @@
|
||||
|www.google.tt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.vg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.vu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|
||||
@@ -1 +1 @@
|
||||
|pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com
|
||||
|pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com
|
||||
|
||||
@@ -49,4 +49,4 @@
|
||||
|yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
@@ -53,44 +54,85 @@ func isServiceProtected(s filtering.SafeSearchConfig, service Service) (ok bool)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultSafeSearch is the default safesearch struct.
|
||||
type DefaultSafeSearch struct {
|
||||
engine *urlfilter.DNSEngine
|
||||
safeSearchCache cache.Cache
|
||||
resolver filtering.Resolver
|
||||
cacheTime time.Duration
|
||||
// Default is the default safe search filter that uses filtering rules with the
|
||||
// dnsrewrite modifier.
|
||||
type Default struct {
|
||||
// mu protects engine.
|
||||
mu *sync.RWMutex
|
||||
|
||||
// engine is the filtering engine that contains the DNS rewrite rules.
|
||||
// engine may be nil, which means that this safe search filter is disabled.
|
||||
engine *urlfilter.DNSEngine
|
||||
|
||||
cache cache.Cache
|
||||
resolver filtering.Resolver
|
||||
logPrefix string
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewDefaultSafeSearch returns new safesearch struct. CacheTime is an element
|
||||
// TTL (in minutes).
|
||||
func NewDefaultSafeSearch(
|
||||
// NewDefault returns an initialized default safe search filter. name is used
|
||||
// for logging.
|
||||
func NewDefault(
|
||||
conf filtering.SafeSearchConfig,
|
||||
name string,
|
||||
cacheSize uint,
|
||||
cacheTime time.Duration,
|
||||
) (ss *DefaultSafeSearch, err error) {
|
||||
engine, err := newEngine(filtering.SafeSearchListID, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheTTL time.Duration,
|
||||
) (ss *Default, err error) {
|
||||
var resolver filtering.Resolver = net.DefaultResolver
|
||||
if conf.CustomResolver != nil {
|
||||
resolver = conf.CustomResolver
|
||||
}
|
||||
|
||||
return &DefaultSafeSearch{
|
||||
engine: engine,
|
||||
safeSearchCache: cache.New(cache.Config{
|
||||
ss = &Default{
|
||||
mu: &sync.RWMutex{},
|
||||
|
||||
cache: cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: cacheSize,
|
||||
}),
|
||||
cacheTime: cacheTime,
|
||||
resolver: resolver,
|
||||
}, nil
|
||||
resolver: resolver,
|
||||
// Use %s, because the client safe-search names already contain double
|
||||
// quotes.
|
||||
logPrefix: fmt.Sprintf("safesearch %s: ", name),
|
||||
cacheTTL: cacheTTL,
|
||||
}
|
||||
|
||||
err = ss.resetEngine(filtering.SafeSearchListID, conf)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
// newEngine creates new engine for provided safe search configuration.
|
||||
func newEngine(listID int, conf filtering.SafeSearchConfig) (engine *urlfilter.DNSEngine, err error) {
|
||||
// log is a helper for logging that includes the name of the safe search
|
||||
// filter. level must be one of [log.DEBUG], [log.INFO], and [log.ERROR].
|
||||
func (ss *Default) log(level log.Level, msg string, args ...any) {
|
||||
switch level {
|
||||
case log.DEBUG:
|
||||
log.Debug(ss.logPrefix+msg, args...)
|
||||
case log.INFO:
|
||||
log.Info(ss.logPrefix+msg, args...)
|
||||
case log.ERROR:
|
||||
log.Error(ss.logPrefix+msg, args...)
|
||||
default:
|
||||
panic(fmt.Errorf("safesearch: unsupported logging level %d", level))
|
||||
}
|
||||
}
|
||||
|
||||
// resetEngine creates new engine for provided safe search configuration and
|
||||
// sets it in ss.
|
||||
func (ss *Default) resetEngine(
|
||||
listID int,
|
||||
conf filtering.SafeSearchConfig,
|
||||
) (err error) {
|
||||
if !conf.Enabled {
|
||||
ss.log(log.INFO, "disabled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for service, serviceRules := range safeSearchRules {
|
||||
if isServiceProtected(conf, service) {
|
||||
@@ -106,20 +148,73 @@ func newEngine(listID int, conf filtering.SafeSearchConfig) (engine *urlfilter.D
|
||||
|
||||
rs, err := filterlist.NewRuleStorage([]filterlist.RuleList{strList})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating rule storage: %w", err)
|
||||
return fmt.Errorf("creating rule storage: %w", err)
|
||||
}
|
||||
|
||||
engine = urlfilter.NewDNSEngine(rs)
|
||||
log.Info("safesearch: filter %d: reset %d rules", listID, engine.RulesCount)
|
||||
ss.engine = urlfilter.NewDNSEngine(rs)
|
||||
|
||||
return engine, nil
|
||||
ss.log(log.INFO, "reset %d rules", ss.engine.RulesCount)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ filtering.SafeSearch = (*DefaultSafeSearch)(nil)
|
||||
var _ filtering.SafeSearch = (*Default)(nil)
|
||||
|
||||
// CheckHost implements the [filtering.SafeSearch] interface for
|
||||
// *DefaultSafeSearch.
|
||||
func (ss *Default) CheckHost(
|
||||
host string,
|
||||
qtype rules.RRType,
|
||||
) (res filtering.Result, err error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
|
||||
}()
|
||||
|
||||
if qtype != dns.TypeA && qtype != dns.TypeAAAA {
|
||||
return filtering.Result{}, fmt.Errorf("unsupported question type %s", dns.Type(qtype))
|
||||
}
|
||||
|
||||
// Check cache. Return cached result if it was found
|
||||
cachedValue, isFound := ss.getCachedResult(host, qtype)
|
||||
if isFound {
|
||||
ss.log(log.DEBUG, "found in cache: %q", host)
|
||||
|
||||
return cachedValue, nil
|
||||
}
|
||||
|
||||
rewrite := ss.searchHost(host, qtype)
|
||||
if rewrite == nil {
|
||||
return filtering.Result{}, nil
|
||||
}
|
||||
|
||||
fltRes, err := ss.newResult(rewrite, qtype)
|
||||
if err != nil {
|
||||
ss.log(log.DEBUG, "looking up addresses for %q: %s", host, err)
|
||||
|
||||
return filtering.Result{}, err
|
||||
}
|
||||
|
||||
if fltRes != nil {
|
||||
res = *fltRes
|
||||
ss.setCacheResult(host, qtype, res)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return filtering.Result{}, fmt.Errorf("no ipv4 addresses for %q", host)
|
||||
}
|
||||
|
||||
// searchHost looks up DNS rewrites in the internal DNS filtering engine.
|
||||
func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRewrite) {
|
||||
ss.mu.RLock()
|
||||
defer ss.mu.RUnlock()
|
||||
|
||||
if ss.engine == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchHost implements the [filtering.SafeSearch] interface for *DefaultSafeSearch.
|
||||
func (ss *DefaultSafeSearch) SearchHost(host string, qtype uint16) (res *rules.DNSRewrite) {
|
||||
r, _ := ss.engine.MatchRequest(&urlfilter.DNSRequest{
|
||||
Hostname: strings.ToLower(host),
|
||||
DNSType: qtype,
|
||||
@@ -133,51 +228,11 @@ func (ss *DefaultSafeSearch) SearchHost(host string, qtype uint16) (res *rules.D
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckHost implements the [filtering.SafeSearch] interface for
|
||||
// *DefaultSafeSearch.
|
||||
func (ss *DefaultSafeSearch) CheckHost(
|
||||
host string,
|
||||
qtype uint16,
|
||||
) (res filtering.Result, err error) {
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("safesearch: lookup for %s", host)
|
||||
}
|
||||
|
||||
// Check cache. Return cached result if it was found
|
||||
cachedValue, isFound := ss.getCachedResult(host)
|
||||
if isFound {
|
||||
log.Debug("safesearch: found in cache: %s", host)
|
||||
|
||||
return cachedValue, nil
|
||||
}
|
||||
|
||||
rewrite := ss.SearchHost(host, qtype)
|
||||
if rewrite == nil {
|
||||
return filtering.Result{}, nil
|
||||
}
|
||||
|
||||
dRes, err := ss.newResult(rewrite, qtype)
|
||||
if err != nil {
|
||||
log.Debug("safesearch: failed to lookup addresses for %s: %s", host, err)
|
||||
|
||||
return filtering.Result{}, err
|
||||
}
|
||||
|
||||
if dRes != nil {
|
||||
res = *dRes
|
||||
ss.setCacheResult(host, res)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return filtering.Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", host)
|
||||
}
|
||||
|
||||
// newResult creates Result object from rewrite rule.
|
||||
func (ss *DefaultSafeSearch) newResult(
|
||||
// newResult creates Result object from rewrite rule. qtype must be either
|
||||
// [dns.TypeA] or [dns.TypeAAAA].
|
||||
func (ss *Default) newResult(
|
||||
rewrite *rules.DNSRewrite,
|
||||
qtype uint16,
|
||||
qtype rules.RRType,
|
||||
) (res *filtering.Result, err error) {
|
||||
res = &filtering.Result{
|
||||
Rules: []*filtering.ResultRule{{
|
||||
@@ -187,7 +242,7 @@ func (ss *DefaultSafeSearch) newResult(
|
||||
IsFiltered: true,
|
||||
}
|
||||
|
||||
if rewrite.RRType == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
|
||||
if rewrite.RRType == qtype {
|
||||
ip, ok := rewrite.Value.(net.IP)
|
||||
if !ok || ip == nil {
|
||||
return nil, nil
|
||||
@@ -198,17 +253,25 @@ func (ss *DefaultSafeSearch) newResult(
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if rewrite.NewCNAME == "" {
|
||||
host := rewrite.NewCNAME
|
||||
if host == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ips, err := ss.resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
||||
ss.log(log.DEBUG, "resolving %q", host)
|
||||
|
||||
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ss.log(log.DEBUG, "resolved %s", ips)
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip = ip.To4(); ip == nil {
|
||||
// TODO(a.garipov): Remove this filtering once the resolver we use
|
||||
// actually learns about network.
|
||||
ip = fitToProto(ip, qtype)
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -220,38 +283,71 @@ func (ss *DefaultSafeSearch) newResult(
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// setCacheResult stores data in cache for host.
|
||||
func (ss *DefaultSafeSearch) setCacheResult(host string, res filtering.Result) {
|
||||
expire := uint32(time.Now().Add(ss.cacheTime).Unix())
|
||||
// qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].
|
||||
// It panics for other types.
|
||||
func qtypeToProto(qtype rules.RRType) (proto string) {
|
||||
switch qtype {
|
||||
case dns.TypeA:
|
||||
return "ip4"
|
||||
case dns.TypeAAAA:
|
||||
return "ip6"
|
||||
default:
|
||||
panic(fmt.Errorf("safesearch: unsupported question type %s", dns.Type(qtype)))
|
||||
}
|
||||
}
|
||||
|
||||
// fitToProto returns a non-nil IP address if ip is the correct protocol version
|
||||
// for qtype. qtype is expected to be either [dns.TypeA] or [dns.TypeAAAA].
|
||||
func fitToProto(ip net.IP, qtype rules.RRType) (res net.IP) {
|
||||
ip4 := ip.To4()
|
||||
if qtype == dns.TypeA {
|
||||
return ip4
|
||||
}
|
||||
|
||||
if ip4 == nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setCacheResult stores data in cache for host. qtype is expected to be either
|
||||
// [dns.TypeA] or [dns.TypeAAAA].
|
||||
func (ss *Default) setCacheResult(host string, qtype rules.RRType, res filtering.Result) {
|
||||
expire := uint32(time.Now().Add(ss.cacheTTL).Unix())
|
||||
exp := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(exp, expire)
|
||||
buf := bytes.NewBuffer(exp)
|
||||
|
||||
err := gob.NewEncoder(buf).Encode(res)
|
||||
if err != nil {
|
||||
log.Error("safesearch: cache encoding: %s", err)
|
||||
ss.log(log.ERROR, "cache encoding: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val := buf.Bytes()
|
||||
_ = ss.safeSearchCache.Set([]byte(host), val)
|
||||
_ = ss.cache.Set([]byte(dns.Type(qtype).String()+" "+host), val)
|
||||
|
||||
log.Debug("safesearch: stored in cache: %s (%d bytes)", host, len(val))
|
||||
ss.log(log.DEBUG, "stored in cache: %q, %d bytes", host, len(val))
|
||||
}
|
||||
|
||||
// getCachedResult returns stored data from cache for host.
|
||||
func (ss *DefaultSafeSearch) getCachedResult(host string) (res filtering.Result, ok bool) {
|
||||
// getCachedResult returns stored data from cache for host. qtype is expected
|
||||
// to be either [dns.TypeA] or [dns.TypeAAAA].
|
||||
func (ss *Default) getCachedResult(
|
||||
host string,
|
||||
qtype rules.RRType,
|
||||
) (res filtering.Result, ok bool) {
|
||||
res = filtering.Result{}
|
||||
|
||||
data := ss.safeSearchCache.Get([]byte(host))
|
||||
data := ss.cache.Get([]byte(dns.Type(qtype).String() + " " + host))
|
||||
if data == nil {
|
||||
return res, false
|
||||
}
|
||||
|
||||
exp := binary.BigEndian.Uint32(data[:4])
|
||||
if exp <= uint32(time.Now().Unix()) {
|
||||
ss.safeSearchCache.Del([]byte(host))
|
||||
ss.cache.Del([]byte(host))
|
||||
|
||||
return res, false
|
||||
}
|
||||
@@ -260,10 +356,27 @@ func (ss *DefaultSafeSearch) getCachedResult(host string) (res filtering.Result,
|
||||
|
||||
err := gob.NewDecoder(buf).Decode(&res)
|
||||
if err != nil {
|
||||
log.Debug("safesearch: cache decoding: %s", err)
|
||||
ss.log(log.ERROR, "cache decoding: %s", err)
|
||||
|
||||
return filtering.Result{}, false
|
||||
}
|
||||
|
||||
return res, true
|
||||
}
|
||||
|
||||
// Update implements the [filtering.SafeSearch] interface for *Default. Update
|
||||
// ignores the CustomResolver and Enabled fields.
|
||||
func (ss *Default) Update(conf filtering.SafeSearchConfig) (err error) {
|
||||
ss.mu.Lock()
|
||||
defer ss.mu.Unlock()
|
||||
|
||||
err = ss.resetEngine(filtering.SafeSearchListID, conf)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
ss.cache.Clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
137
internal/filtering/safesearch/safesearch_internal_test.go
Normal file
137
internal/filtering/safesearch/safesearch_internal_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package safesearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO(a.garipov): Move as much of this as possible into proper external tests.
|
||||
|
||||
const (
|
||||
// TODO(a.garipov): Add IPv6 tests.
|
||||
testQType = dns.TypeA
|
||||
testCacheSize = 5000
|
||||
testCacheTTL = 30 * time.Minute
|
||||
)
|
||||
|
||||
var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
||||
Enabled: true,
|
||||
Bing: true,
|
||||
DuckDuckGo: true,
|
||||
Google: true,
|
||||
Pixabay: true,
|
||||
Yandex: true,
|
||||
YouTube: true,
|
||||
}
|
||||
|
||||
var yandexIP = net.IPv4(213, 180, 193, 56)
|
||||
|
||||
func newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *Default) {
|
||||
ss, err := NewDefault(ssConf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
func TestSafeSearch(t *testing.T) {
|
||||
ss := newForTest(t, defaultSafeSearchConf)
|
||||
val := ss.searchHost("www.google.com", testQType)
|
||||
|
||||
assert.Equal(t, &rules.DNSRewrite{NewCNAME: "forcesafesearch.google.com"}, val)
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheYandex(t *testing.T) {
|
||||
const domain = "yandex.ru"
|
||||
|
||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||
|
||||
// Check host with disabled safesearch.
|
||||
res, err := ss.CheckHost(domain, testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
assert.Empty(t, res.Rules)
|
||||
|
||||
ss = newForTest(t, defaultSafeSearchConf)
|
||||
res, err = ss.CheckHost(domain, testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
// For yandex we already know valid IP.
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, res.Rules[0].IP, yandexIP)
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := ss.getCachedResult(domain, testQType)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||
const domain = "www.google.ru"
|
||||
|
||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||
|
||||
res, err := ss.CheckHost(domain, testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
assert.Empty(t, res.Rules)
|
||||
|
||||
resolver := &aghtest.TestResolver{}
|
||||
ss = newForTest(t, defaultSafeSearchConf)
|
||||
ss.resolver = resolver
|
||||
|
||||
// Lookup for safesearch domain.
|
||||
rewrite := ss.searchHost(domain, testQType)
|
||||
|
||||
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundIP net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
foundIP = ip
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res, err = ss.CheckHost(domain, testQType)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.True(t, res.Rules[0].IP.Equal(foundIP))
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := ss.getCachedResult(domain, testQType)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
|
||||
}
|
||||
|
||||
const googleHost = "www.google.com"
|
||||
|
||||
var dnsRewriteSink *rules.DNSRewrite
|
||||
|
||||
func BenchmarkSafeSearch(b *testing.B) {
|
||||
ss := newForTest(b, defaultSafeSearchConf)
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
dnsRewriteSink = ss.searchHost(googleHost, testQType)
|
||||
}
|
||||
|
||||
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteSink.NewCNAME)
|
||||
}
|
||||
@@ -1,26 +1,37 @@
|
||||
package safesearch
|
||||
package safesearch_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutil.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// Common test constants.
|
||||
const (
|
||||
safeSearchCacheSize = 5000
|
||||
cacheTime = 30 * time.Minute
|
||||
// TODO(a.garipov): Add IPv6 tests.
|
||||
testQType = dns.TypeA
|
||||
testCacheSize = 5000
|
||||
testCacheTTL = 30 * time.Minute
|
||||
)
|
||||
|
||||
var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
||||
Enabled: true,
|
||||
// testConf is the default safe search configuration for tests.
|
||||
var testConf = filtering.SafeSearchConfig{
|
||||
CustomResolver: nil,
|
||||
|
||||
Enabled: true,
|
||||
|
||||
Bing: true,
|
||||
DuckDuckGo: true,
|
||||
Google: true,
|
||||
@@ -29,25 +40,15 @@ var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
||||
YouTube: true,
|
||||
}
|
||||
|
||||
// yandexIP is the expected IP address of Yandex safe search results. Keep in
|
||||
// sync with the rules data.
|
||||
var yandexIP = net.IPv4(213, 180, 193, 56)
|
||||
|
||||
func newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *DefaultSafeSearch) {
|
||||
ss, err := NewDefaultSafeSearch(ssConf, safeSearchCacheSize, cacheTime)
|
||||
func TestDefault_CheckHost_yandex(t *testing.T) {
|
||||
conf := testConf
|
||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
func TestSafeSearch(t *testing.T) {
|
||||
ss := newForTest(t, defaultSafeSearchConf)
|
||||
val := ss.SearchHost("www.google.com", dns.TypeA)
|
||||
|
||||
assert.Equal(t, &rules.DNSRewrite{NewCNAME: "forcesafesearch.google.com"}, val)
|
||||
}
|
||||
|
||||
func TestCheckHostSafeSearchYandex(t *testing.T) {
|
||||
ss := newForTest(t, defaultSafeSearchConf)
|
||||
|
||||
// Check host for each domain.
|
||||
for _, host := range []string{
|
||||
"yandex.ru",
|
||||
@@ -57,7 +58,8 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
|
||||
"yandex.kz",
|
||||
"www.yandex.com",
|
||||
} {
|
||||
res, err := ss.CheckHost(host, dns.TypeA)
|
||||
var res filtering.Result
|
||||
res, err = ss.CheckHost(host, testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
@@ -69,12 +71,14 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
||||
func TestDefault_CheckHost_google(t *testing.T) {
|
||||
resolver := &aghtest.TestResolver{}
|
||||
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
||||
|
||||
ss := newForTest(t, defaultSafeSearchConf)
|
||||
ss.resolver = resolver
|
||||
conf := testConf
|
||||
conf.CustomResolver = resolver
|
||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check host for each domain.
|
||||
for _, host := range []string{
|
||||
@@ -87,7 +91,8 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
||||
"www.google.je",
|
||||
} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
res, err := ss.CheckHost(host, dns.TypeA)
|
||||
var res filtering.Result
|
||||
res, err = ss.CheckHost(host, testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
@@ -100,103 +105,35 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheYandex(t *testing.T) {
|
||||
const domain = "yandex.ru"
|
||||
|
||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||
|
||||
// Check host with disabled safesearch.
|
||||
res, err := ss.CheckHost(domain, dns.TypeA)
|
||||
func TestDefault_Update(t *testing.T) {
|
||||
conf := testConf
|
||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
assert.Empty(t, res.Rules)
|
||||
|
||||
ss = newForTest(t, defaultSafeSearchConf)
|
||||
res, err = ss.CheckHost(domain, dns.TypeA)
|
||||
res, err := ss.CheckHost("www.yandex.com", testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
// For yandex we already know valid IP.
|
||||
require.Len(t, res.Rules, 1)
|
||||
assert.True(t, res.IsFiltered)
|
||||
|
||||
assert.Equal(t, res.Rules[0].IP, yandexIP)
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := ss.getCachedResult(domain)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||
const domain = "www.google.ru"
|
||||
|
||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||
|
||||
res, err := ss.CheckHost(domain, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
assert.Empty(t, res.Rules)
|
||||
|
||||
resolver := &aghtest.TestResolver{}
|
||||
ss = newForTest(t, defaultSafeSearchConf)
|
||||
ss.resolver = resolver
|
||||
|
||||
// Lookup for safesearch domain.
|
||||
rewrite := ss.SearchHost(domain, dns.TypeA)
|
||||
|
||||
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundIP net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
foundIP = ip
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res, err = ss.CheckHost(domain, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.True(t, res.Rules[0].IP.Equal(foundIP))
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := ss.getCachedResult(domain)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
|
||||
}
|
||||
|
||||
const googleHost = "www.google.com"
|
||||
|
||||
var dnsRewriteSink *rules.DNSRewrite
|
||||
|
||||
func BenchmarkSafeSearch(b *testing.B) {
|
||||
ss := newForTest(b, defaultSafeSearchConf)
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
dnsRewriteSink = ss.SearchHost(googleHost, dns.TypeA)
|
||||
}
|
||||
|
||||
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteSink.NewCNAME)
|
||||
}
|
||||
|
||||
var dnsRewriteParallelSink *rules.DNSRewrite
|
||||
|
||||
func BenchmarkSafeSearch_parallel(b *testing.B) {
|
||||
ss := newForTest(b, defaultSafeSearchConf)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
dnsRewriteParallelSink = ss.SearchHost(googleHost, dns.TypeA)
|
||||
}
|
||||
err = ss.Update(filtering.SafeSearchConfig{
|
||||
Enabled: true,
|
||||
Google: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteParallelSink.NewCNAME)
|
||||
res, err = ss.CheckHost("www.yandex.com", testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
|
||||
err = ss.Update(filtering.SafeSearchConfig{
|
||||
Enabled: false,
|
||||
Google: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err = ss.CheckHost("www.yandex.com", testQType)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
}
|
||||
|
||||
71
internal/filtering/safesearchhttp.go
Normal file
71
internal/filtering/safesearchhttp.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package filtering
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
)
|
||||
|
||||
// handleSafeSearchEnable is the handler for POST /control/safesearch/enable
|
||||
// HTTP API.
|
||||
//
|
||||
// Deprecated: Use handleSafeSearchSettings.
|
||||
func (d *DNSFilter) handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
|
||||
setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, true)
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
// handleSafeSearchDisable is the handler for POST /control/safesearch/disable
|
||||
// HTTP API.
|
||||
//
|
||||
// Deprecated: Use handleSafeSearchSettings.
|
||||
func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
|
||||
setProtectedBool(&d.confLock, &d.Config.SafeSearchConf.Enabled, false)
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
// handleSafeSearchStatus is the handler for GET /control/safesearch/status
|
||||
// HTTP API.
|
||||
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var resp SafeSearchConfig
|
||||
func() {
|
||||
d.confLock.RLock()
|
||||
defer d.confLock.RUnlock()
|
||||
|
||||
resp = d.Config.SafeSearchConf
|
||||
}()
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// handleSafeSearchSettings is the handler for PUT /control/safesearch/settings
|
||||
// HTTP API.
|
||||
func (d *DNSFilter) handleSafeSearchSettings(w http.ResponseWriter, r *http.Request) {
|
||||
req := &SafeSearchConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
conf := *req
|
||||
err = d.safeSearch.Update(conf)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "updating: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func() {
|
||||
d.confLock.Lock()
|
||||
defer d.confLock.Unlock()
|
||||
|
||||
d.Config.SafeSearchConf = conf
|
||||
}()
|
||||
|
||||
d.Config.ConfigModified()
|
||||
|
||||
aghhttp.OK(w)
|
||||
}
|
||||
@@ -311,6 +311,14 @@ var blockedServices = []blockedService{{
|
||||
"||warp.plus^",
|
||||
"||workers.dev^",
|
||||
},
|
||||
}, {
|
||||
ID: "crunchyroll",
|
||||
Name: "Crunchyroll",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M 25 3 C 12.85 3 3 12.85 3 25 C 3 40.188 13.387672 44.538609 20.388672 45.974609 C 20.427672 45.982609 20.465953 45.986328 20.501953 45.986328 C 21.006953 45.986328 21.206312 45.25525 20.695312 45.03125 C 13.285312 41.79025 8.0301562 34.327141 9.1601562 25.494141 C 10.256156 16.920141 17.244938 10.069141 25.835938 9.1191406 C 26.564937 9.0381406 27.287 9 28 9 C 35.541 9 42.044422 13.395672 45.107422 19.763672 C 45.206422 19.968672 45.382594 20.058594 45.558594 20.058594 C 45.853594 20.058594 46.144828 19.8075 46.048828 19.4375 C 44.302828 12.7105 39 3 25 3 z M 29 14 C 20.481 14 13.619625 21.101031 14.015625 29.707031 C 14.366625 37.346031 20.653016 43.631422 28.291016 43.982422 C 28.528016 43.994422 28.766 44 29 44 C 37.285 44 44 37.285 44 29 C 44 27.819 43.860563 26.670359 43.601562 25.568359 C 43.542563 25.319359 43.332234 25.183594 43.115234 25.183594 C 42.961234 25.183594 42.806266 25.251484 42.697266 25.396484 C 41.512266 26.976484 39.627 28 37.5 28 C 37.397 28 37.293453 27.997188 37.189453 27.992188 C 34.031453 27.845188 31.348203 25.317875 31.033203 22.171875 C 30.763203 19.477875 32.142297 17.082328 34.279297 15.861328 C 34.656297 15.646328 34.62475 15.100266 34.21875 14.947266 C 32.59375 14.340266 30.838 14 29 14 z M 44.296875 26.595703 L 44.300781 26.595703 L 44.296875 26.595703 z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||crunchyroll.com^",
|
||||
"||gccrunchyroll.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "dailymotion",
|
||||
Name: "Dailymotion",
|
||||
@@ -1182,6 +1190,24 @@ var blockedServices = []blockedService{{
|
||||
"||gog.com^",
|
||||
"||gogalaxy.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "hbomax",
|
||||
Name: "HBO Max",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M0 14v22h5v-9h3v9h5V14H8v8H5v-8H0zm15 0v22h8.4c3.1 0 5.7-2.3 6.2-5.4a11 11 0 1 0 0-11.2c-.5-3-3-5.4-6.2-5.4H15zm5 5h3a2 2 0 1 1 0 4h-3v-4zm19 0a6 6 0 1 1 0 12 6 6 0 0 1 0-12zm0 2a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4zm-11 2.8v2.4c-.4-.5-1-1-2-1.2 1-.3 1.7-.8 2-1.3zm-8 4h3a2 2 0 1 1 0 4h-3v-4z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||hbo.com^",
|
||||
"||hbogo.co.th^",
|
||||
"||hbogo.com^",
|
||||
"||hbogo.eu^",
|
||||
"||hbogoasia.com^",
|
||||
"||hbogoasia.id^",
|
||||
"||hbogoasia.ph^",
|
||||
"||hbomax-images.warnermediacdn.com^",
|
||||
"||hbomax.com^",
|
||||
"||hbomaxcdn.com^",
|
||||
"||hbonow.com^",
|
||||
"||maxgo.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "hulu",
|
||||
Name: "Hulu",
|
||||
@@ -1299,6 +1325,21 @@ var blockedServices = []blockedService{{
|
||||
"||kakao.com^",
|
||||
"||kgslb.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "lazada",
|
||||
Name: "Lazada",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 100 100\"><path d=\"M26 17a2 2 0 0 0-1.1.3L8.5 27.4A3 3 0 0 0 7 30v29.1c0 1 .6 2 1.5 2.6l40 24.8c.2.3.6.4 1 .5h1a3 3 0 0 0 1-.5l40-24.8a3 3 0 0 0 1.5-2.6v-29c0-1.1-.6-2.1-1.5-2.7l-16.4-10a2.1 2.1 0 0 0-2.2 0l-15.4 9.4a14.3 14.3 0 0 1-15 0L27 17.3c-.3-.2-.7-.3-1.1-.3zm0 2 15.4 9.5a16.3 16.3 0 0 0 17.2 0L74 19l16.5 10.1v.1L50 51.4 9.4 29.2h.1L26 19zm48 4c-.4 0-.9 0-1.3.3l-5.5 3.4a.5.5 0 1 0 .6.9l5.4-3.4c.5-.3 1.1-.3 1.6 0l9.4 5.7a.5.5 0 1 0 .5-.8l-9.4-5.8c-.4-.2-.8-.4-1.3-.4zm-8.7 5a.5.5 0 0 0-.3 0l-1.6 1a.5.5 0 0 0 .6 1l1.6-1a.5.5 0 0 0-.3-1zM9 30.1l40.5 22.2v32.6L9.5 60a1 1 0 0 1-.5-.9v-29zm82 0v29c0 .4-.2.7-.5 1l-40 24.7V52.4L91 30.1zM12.5 35a.5.5 0 0 0-.5.5v21.2c0 .8.4 1.6 1.2 2l16 10a.5.5 0 1 0 .6-.8l-16-10c-.5-.2-.8-.7-.8-1.2V35.5a.5.5 0 0 0-.5-.5zm24 37.2a.5.5 0 0 0-.3.9l4 2.5a.5.5 0 1 0 .6-.9l-4-2.4a.5.5 0 0 0-.3-.1zm7 4.3a.5.5 0 0 0-.3 1l1 .6a.5.5 0 1 0 .6-.9l-1-.6a.5.5 0 0 0-.3 0z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||k1-lazadasg-oversea.gslb.ksyuncdn.com^",
|
||||
"||lazada.co.id^",
|
||||
"||lazada.co.th^",
|
||||
"||lazada.com.my^",
|
||||
"||lazada.com.ph^",
|
||||
"||lazada.com^",
|
||||
"||lazada.sg^",
|
||||
"||lazada.vn^",
|
||||
"||slatic.net^",
|
||||
},
|
||||
}, {
|
||||
ID: "leagueoflegends",
|
||||
Name: "League of Legends",
|
||||
@@ -1383,7 +1424,6 @@ var blockedServices = []blockedService{{
|
||||
"||mastodon.sdf.org^",
|
||||
"||mastodon.social^",
|
||||
"||mastodon.social^",
|
||||
"||mastodon.top^",
|
||||
"||mastodon.uno^",
|
||||
"||mastodon.world^",
|
||||
"||mastodon.xyz^",
|
||||
@@ -1400,6 +1440,7 @@ var blockedServices = []blockedService{{
|
||||
"||mstdn.jp^",
|
||||
"||mstdn.social^",
|
||||
"||muenchen.social^",
|
||||
"||muenster.im^",
|
||||
"||newsie.social^",
|
||||
"||noc.social^",
|
||||
"||norden.social^",
|
||||
@@ -1428,6 +1469,7 @@ var blockedServices = []blockedService{{
|
||||
"||techhub.social^",
|
||||
"||theblower.au^",
|
||||
"||tkz.one^",
|
||||
"||todon.eu^",
|
||||
"||toot.aquilenet.fr^",
|
||||
"||toot.community^",
|
||||
"||toot.funami.tech^",
|
||||
@@ -1438,7 +1480,6 @@ var blockedServices = []blockedService{{
|
||||
"||union.place^",
|
||||
"||universeodon.com^",
|
||||
"||urbanists.social^",
|
||||
"||vocalodon.net^",
|
||||
"||wien.rocks^",
|
||||
"||wxw.moe^",
|
||||
},
|
||||
@@ -1568,6 +1609,20 @@ var blockedServices = []blockedService{{
|
||||
"||pinterest.vn^",
|
||||
"||pinterestmail.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "playstation",
|
||||
Name: "PlayStation",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 30 30\"><path d=\"M11.18 3.74v21.12l4.58 1.4V8.58c0-.51 0-.77.26-1.02.12-.26.38-.26.63-.13.64.26 1.02.76 1.02 1.78v7c1.53.76 2.8.76 3.81 0 1.02-.77 1.53-1.9 1.53-3.82 0-2.03-.38-3.3-1.27-4.32-.76-1.02-2.16-1.91-4.2-2.55-2.54-.76-4.7-1.4-6.36-1.78zM9.91 16.97l-5.85 2.04-.89.38c-1.4.63-2.16 1.27-2.16 1.9.12.77.38 1.79 2.29 2.42 1.78.64 3.18.9 6.74-.12v-2.3c-3.44 1.15-3.95 1.02-4.45.77-.51-.25-.51-.5-.39-.64.39-.25 1.78-.76 1.78-.76l2.93-1.02v-2.67zm12.94 1c-.41-.02-.82-.01-1.24.02-1.4 0-2.67.25-4.2.64v2.67l2.8-1.02 1.53-.51s.64-.13 1.02-.25c.63-.13 1.4.12 1.4.12.38 0 .63.13.63.38.13.26-.12.39-.76.64l-1.4.51-5.09 1.9v2.68l2.3-.77 6.35-2.28.77-.39c1.52-.5 2.16-1.14 2.03-1.9 0-.77-.89-1.28-2.42-1.79a14.28 14.28 0 0 0-3.72-.66z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||gaikai.com",
|
||||
"||playstation-cloud.com",
|
||||
"||playstation-cloud.net",
|
||||
"||playstation.com",
|
||||
"||playstation.net",
|
||||
"||scea.com",
|
||||
"||sonyentertainmentnetwork.com",
|
||||
"||station.sony.com",
|
||||
},
|
||||
}, {
|
||||
ID: "qq",
|
||||
Name: "QQ",
|
||||
@@ -1597,6 +1652,18 @@ var blockedServices = []blockedService{{
|
||||
"||redditmedia.com^",
|
||||
"||redditstatic.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "riot_games",
|
||||
Name: "Riot Games",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 64 64\"><path d=\"M31.3 2.2a1 1 0 0 0-.8 1.4l.8 1.8a1 1 0 1 0 1.8-.8l-.8-1.8a1 1 0 0 0-1-.6zm-4.3 2a1 1 0 0 0-.9 1.5l1 1.8a1 1 0 1 0 1.7-.9L28 4.8a1 1 0 0 0-1-.6zm12 1a1 1 0 0 0-.4.1l-34 16.2a1 1 0 0 0-.6 1.1L9.3 47a1 1 0 0 0 1 .8h7a1 1 0 0 0 1-1l-1-12.2 3 12.4a1 1 0 0 0 .9.8h7.3a1 1 0 0 0 1-1l-.2-15.8L32 47a1 1 0 0 0 1 .8h7.6a1 1 0 0 0 1-1l1.3-19.4 1.4 19.5a1 1 0 0 0 1 1h10.2a1 1 0 0 0 1-1L60 11.2a1 1 0 0 0-.8-1l-20-5a1 1 0 0 0-.3 0zm-16.3 1a1 1 0 0 0-.9 1.5l.9 1.8a1 1 0 1 0 1.8-.8l-.9-1.8a1 1 0 0 0-1-.6zm16.4 1L57.9 12l-3.3 33.8h-8.3l-1.9-25a1 1 0 0 0-1.2-.8l-1.2.3a1 1 0 0 0-.7.9l-1.7 24.6h-5.8L30.3 25a1 1 0 0 0-1.3-.8l-1 .4a1 1 0 0 0-.8 1l.3 20.2H22l-4-17a1 1 0 0 0-1.2-.7l-1.1.3a1 1 0 0 0-.7 1l1.2 16.4H11L6.1 23l33-15.7zM18.5 8.4a1 1 0 0 0-1 1.5l.9 1.8a1 1 0 1 0 1.8-.9L19.3 9a1 1 0 0 0-.8-.6zm-4.3 2.1a1 1 0 0 0-.1 0 1 1 0 0 0-.9 1.4l.9 1.8a1 1 0 1 0 1.8-.8L15 11a1 1 0 0 0-.8-.6zm-4.4 2a1 1 0 0 0-.9 1.5l.9 1.8a1 1 0 1 0 1.8-.9l-.9-1.8a1 1 0 0 0-1-.5zm-4.3 2.1a1 1 0 0 0-.9 1.4l.9 1.9a1 1 0 1 0 1.8-1l-.9-1.7a1 1 0 0 0-.9-.6zM30.7 49a1 1 0 0 0-.9 1.4l2.5 6.5a1 1 0 0 0 .7.6l20.7 5.3a1 1 0 0 0 1.2-.9L56 51.4a1 1 0 0 0-1-1L30.9 49a1 1 0 0 0-.1 0zm1.5 2.1L54 52.3l-.9 8.2-19-4.8-1.8-4.6z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||dradis-prod.rdatasrv.net^",
|
||||
"||pvp.net^",
|
||||
"||rgpub.io^",
|
||||
"||riotcdn.com^",
|
||||
"||riotcdn.net^",
|
||||
"||riotgames.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "roblox",
|
||||
Name: "Roblox",
|
||||
@@ -1610,6 +1677,32 @@ var blockedServices = []blockedService{{
|
||||
"||robloxcdn.com^",
|
||||
"||robloxdev.cn^",
|
||||
},
|
||||
}, {
|
||||
ID: "shopee",
|
||||
Name: "Shopee",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M25 1c-5.3 0-9.4 5-9.8 11H5a2 2 0 0 0-2 2.1l1.7 30.2a5 5 0 0 0 5 4.7h30.4a5 5 0 0 0 5-4.7L47 14a2 2 0 0 0-2-2.1H35C34.3 6 30.2 1 25 1zm0 2c4 0 7.4 3.9 7.8 9H17.2c.4-5.1 3.8-9 7.8-9zM5 14h10.8a1 1 0 0 0 .4 0h17.6a1 1 0 0 0 .4 0h10.7l-1.7 30.2a3 3 0 0 1-3 2.8H9.8a3 3 0 0 1-3-2.8L5 14zm20 4c-4.2 0-7.5 2.7-7.5 6.3 0 4 3.8 5.4 7 6.6 4 1.4 6.5 2.5 6.5 5.7 0 2.4-2.7 4.4-6 4.4-3.8 0-7-2.7-7-2.7l-1.2 1.6c.8.7 4.1 3.1 8.1 3.1 4.5 0 8-2.8 8-6.4 0-4.8-4-6.3-7.7-7.6-3.5-1.3-5.7-2.3-5.7-4.7 0-2.5 2.3-4.3 5.6-4.3a11 11 0 0 1 6 1.9l1-1.7c-.3-.1-3.2-2.2-7-2.2z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||shopee.cl^",
|
||||
"||shopee.cn^",
|
||||
"||shopee.co.id^",
|
||||
"||shopee.co.th^",
|
||||
"||shopee.com.br^",
|
||||
"||shopee.com.co^",
|
||||
"||shopee.com.mx^",
|
||||
"||shopee.com.my^",
|
||||
"||shopee.com^",
|
||||
"||shopee.es^",
|
||||
"||shopee.fr^",
|
||||
"||shopee.id^",
|
||||
"||shopee.in^",
|
||||
"||shopee.io^",
|
||||
"||shopee.ph^",
|
||||
"||shopee.sg^",
|
||||
"||shopee.tw^",
|
||||
"||shopee.vn^",
|
||||
"||shopeemobile.com^",
|
||||
"||shp.ee^",
|
||||
},
|
||||
}, {
|
||||
ID: "skype",
|
||||
Name: "Skype",
|
||||
@@ -1813,6 +1906,15 @@ var blockedServices = []blockedService{{
|
||||
"||twvid.com^",
|
||||
"||vine.co^",
|
||||
},
|
||||
}, {
|
||||
ID: "valorant",
|
||||
Name: "Valorant",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M4 6a1 1 0 0 0-1 1v18a1 1 0 0 0 .2.6l14 17a1 1 0 0 0 .8.4h14a1 1 0 0 0 .8-1.6l-28-35A1 1 0 0 0 4 6zm42 1a1 1 0 0 0-.8.4l-18 22A1 1 0 0 0 28 31h14a1 1 0 0 0 .8-.4l4-5a1 1 0 0 0 .2-.6V8a1 1 0 0 0-1-1zM5 9.9 30 41H18.4L5 24.6V10zm40 .9v13.8L41.5 29H30.1L45 10.8z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||playvalorant.com",
|
||||
"||valorant.scd.riotcdn.net",
|
||||
"||valorant.secure.dyn.riotcdn.net",
|
||||
},
|
||||
}, {
|
||||
ID: "viber",
|
||||
Name: "Viber",
|
||||
@@ -1869,6 +1971,13 @@ var blockedServices = []blockedService{{
|
||||
"||vkuservideo.com^",
|
||||
"||vkuservideo.net^",
|
||||
},
|
||||
}, {
|
||||
ID: "voot",
|
||||
Name: "Voot",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 512 512\"><path d=\"M96 340c-1 4-4 6-7 6H48c-3 0-6-2-8-6L0 213c-1-2 0-5 2-5l2-1h30c4 0 7 3 8 6l25 87c1 3 2 3 3 0l25-87c1-3 4-6 7-6h31c2 0 4 2 4 4v2L96 340zm46-50v-32c0-29 14-56 63-56s63 27 63 56v32c0 28-14 56-63 56s-63-28-63-56zm85 1v-35c0-13-7-20-22-20s-22 7-22 20v35c0 13 7 20 22 20s22-7 22-20zm54-1v-32c0-29 14-56 63-56s63 27 63 56v32c0 28-14 56-63 56s-63-28-63-56zm85 1v-35c0-13-7-20-22-20s-21 7-21 20v35c0 13 6 20 21 20s22-7 22-20zm144 44-2-17-1-2c-1-3-3-5-6-5h-2l-10 1c-2 1-4 0-6-2l-2-11v-56c0-3 2-6 6-6h17c4 0 6-2 7-5l1-22c0-3-2-5-5-6h-21c-3 0-5-2-5-5v-28c0-3-2-5-5-5h-1l-30 4c-3 1-5 4-5 7v22c0 3-3 5-6 5h-7c-4 0-6 3-6 6v22c0 3 2 5 6 5h7c3 0 6 3 6 6v67c0 26 15 36 42 36 8 0 16-1 23-4h1c2 0 5-3 5-6l-1-1z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||voot.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "wechat",
|
||||
Name: "WeChat",
|
||||
|
||||
Reference in New Issue
Block a user