all: sync with master

This commit is contained in:
Ainar Garipov
2024-01-30 18:43:51 +03:00
parent f6ad64bf69
commit b01c10b73e
196 changed files with 3190 additions and 1790 deletions

View File

@@ -1,7 +1,6 @@
package filtering
import (
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
@@ -95,39 +94,3 @@ func (d *DNSFilter) processDNSResultRewrites(
return res
}
// appendRewriteResultFromHost appends the rewrite result from rec to vals and
// resRules.
func appendRewriteResultFromHost(
vals []rules.RRValue,
resRules []*ResultRule,
rec *hostsfile.Record,
qtype uint16,
) (updatedVals []rules.RRValue, updatedRules []*ResultRule) {
switch qtype {
case dns.TypeA:
if !rec.Addr.Is4() {
return vals, resRules
}
vals = append(vals, rec.Addr)
case dns.TypeAAAA:
if !rec.Addr.Is6() {
return vals, resRules
}
vals = append(vals, rec.Addr)
case dns.TypePTR:
for _, name := range rec.Names {
vals = append(vals, name)
}
}
recText, _ := rec.MarshalText()
resRules = append(resRules, &ResultRule{
FilterListID: SysHostsListID,
Text: string(recText),
})
return vals, resRules
}

View File

@@ -1,17 +1,11 @@
package filtering
import (
"fmt"
"net/netip"
"path"
"testing"
"testing/fstest"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -215,154 +209,3 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) {
assert.Equal(t, "new-ptr-with-dot.", ptr)
})
}
func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
addrv4 := netip.MustParseAddr("1.2.3.4")
addrv6 := netip.MustParseAddr("::1")
addrMapped := netip.MustParseAddr("::ffff:1.2.3.4")
data := fmt.Sprintf(
""+
"%s v4.host.example\n"+
"%s v6.host.example\n"+
"%s mapped.host.example\n",
addrv4,
addrv6,
addrMapped,
)
files := fstest.MapFS{
"hosts": &fstest.MapFile{
Data: []byte(data),
},
}
watcher := &aghtest.FSWatcher{
OnEvents: func() (e <-chan struct{}) { return nil },
OnAdd: func(name string) (err error) { return nil },
OnClose: func() (err error) { return nil },
}
hc, err := aghnet.NewHostsContainer(files, watcher, "hosts")
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, hc.Close)
f, _ := newForTest(t, &Config{EtcHosts: hc}, nil)
setts := &Settings{
FilteringEnabled: true,
}
testCases := []struct {
name string
host string
wantRules []*ResultRule
wantResps []rules.RRValue
dtyp uint16
}{{
name: "v4",
host: "v4.host.example",
dtyp: dns.TypeA,
wantRules: []*ResultRule{{
Text: "1.2.3.4 v4.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{addrv4},
}, {
name: "v6",
host: "v6.host.example",
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: "::1 v6.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{addrv6},
}, {
name: "mapped",
host: "mapped.host.example",
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: "::ffff:1.2.3.4 mapped.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{addrMapped},
}, {
name: "ptr",
host: "4.3.2.1.in-addr.arpa",
dtyp: dns.TypePTR,
wantRules: []*ResultRule{{
Text: "1.2.3.4 v4.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{"v4.host.example"},
}, {
name: "ptr-mapped",
host: "4.0.3.0.2.0.1.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
dtyp: dns.TypePTR,
wantRules: []*ResultRule{{
Text: "::ffff:1.2.3.4 mapped.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{"mapped.host.example"},
}, {
name: "not_found_v4",
host: "non.existent.example",
dtyp: dns.TypeA,
wantRules: nil,
wantResps: nil,
}, {
name: "not_found_v6",
host: "non.existent.example",
dtyp: dns.TypeAAAA,
wantRules: nil,
wantResps: nil,
}, {
name: "not_found_ptr",
host: "4.3.2.2.in-addr.arpa",
dtyp: dns.TypePTR,
wantRules: nil,
wantResps: nil,
}, {
name: "v4_mismatch",
host: "v4.host.example",
dtyp: dns.TypeAAAA,
wantRules: nil,
wantResps: nil,
}, {
name: "v6_mismatch",
host: "v6.host.example",
dtyp: dns.TypeA,
wantRules: nil,
wantResps: nil,
}, {
name: "wrong_ptr",
host: "4.3.2.1.ip6.arpa",
dtyp: dns.TypePTR,
wantRules: nil,
wantResps: nil,
}, {
name: "unsupported_type",
host: "v4.host.example",
dtyp: dns.TypeCNAME,
wantRules: nil,
wantResps: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var res Result
res, err = f.CheckHost(tc.host, tc.dtyp, setts)
require.NoError(t, err)
if len(tc.wantRules) == 0 {
assert.Empty(t, res.Rules)
assert.Nil(t, res.DNSRewriteResult)
return
}
require.NotNil(t, res.DNSRewriteResult)
require.Contains(t, res.DNSRewriteResult.Response, tc.dtyp)
assert.Equal(t, tc.wantResps, res.DNSRewriteResult.Response[tc.dtyp])
assert.Equal(t, tc.wantRules, res.Rules)
})
}
}

View File

@@ -18,13 +18,11 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/urlfilter"
@@ -100,7 +98,7 @@ type Config struct {
// system configuration files (e.g. /etc/hosts).
//
// TODO(e.burkov): Move it to dnsforward entirely.
EtcHosts *aghnet.HostsContainer `yaml:"-"`
EtcHosts hostsfile.Storage `yaml:"-"`
// Called when the configuration is changed by HTTP request
ConfigModified func() `yaml:"-"`
@@ -482,15 +480,6 @@ func (d *DNSFilter) SetProtectionEnabled(status bool) {
d.conf.ProtectionEnabled = status
}
// EtcHostsRecords returns the hosts records for the hostname.
func (d *DNSFilter) EtcHostsRecords(hostname string) (recs []*hostsfile.Record) {
if d.conf.EtcHosts != nil {
return d.conf.EtcHosts.MatchName(hostname)
}
return recs
}
// SetBlockingMode sets blocking mode properties.
func (d *DNSFilter) SetBlockingMode(mode BlockingMode, bIPv4, bIPv6 netip.Addr) {
d.confMu.Lock()
@@ -628,62 +617,6 @@ func (d *DNSFilter) CheckHost(
return Result{}, nil
}
// matchSysHosts tries to match the host against the operating system's hosts
// database. err is always nil.
func (d *DNSFilter) matchSysHosts(
host string,
qtype uint16,
setts *Settings,
) (res Result, err error) {
// TODO(e.burkov): Where else is this checked?
if !setts.FilteringEnabled || d.conf.EtcHosts == nil {
return res, nil
}
var recs []*hostsfile.Record
switch qtype {
case dns.TypeA, dns.TypeAAAA:
recs = d.conf.EtcHosts.MatchName(host)
case dns.TypePTR:
var ip net.IP
ip, err = netutil.IPFromReversedAddr(host)
if err != nil {
log.Debug("filtering: failed to parse PTR record %q: %s", host, err)
return res, nil
}
addr, _ := netip.AddrFromSlice(ip)
recs = d.conf.EtcHosts.MatchAddr(addr)
default:
log.Debug("filtering: unsupported query type %s", dns.Type(qtype))
}
var vals []rules.RRValue
var resRules []*ResultRule
resRulesLen := 0
for _, rec := range recs {
vals, resRules = appendRewriteResultFromHost(vals, resRules, rec, qtype)
if len(resRules) > resRulesLen {
resRulesLen = len(resRules)
log.Debug("filtering: matched %s in %q", host, rec.Source)
}
}
if len(vals) > 0 {
res.DNSRewriteResult = &DNSRewriteResult{
Response: DNSRewriteResultResponse{
qtype: vals,
},
RCode: dns.RcodeSuccess,
}
res.Rules = resRules
res.Reason = RewrittenRule
}
return res, nil
}
// processRewrites performs filtering based on the legacy rewrite records.
//
// Firstly, it finds CNAME rewrites for host. If the CNAME is the same as host,

View File

@@ -0,0 +1,92 @@
package filtering
import (
"fmt"
"net/netip"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
)
// matchSysHosts tries to match the host against the operating system's hosts
// database. err is always nil.
func (d *DNSFilter) matchSysHosts(
host string,
qtype uint16,
setts *Settings,
) (res Result, err error) {
// TODO(e.burkov): Where else is this checked?
if !setts.FilteringEnabled || d.conf.EtcHosts == nil {
return Result{}, nil
}
vals, rs, matched := hostsRewrites(qtype, host, d.conf.EtcHosts)
if !matched {
return Result{}, nil
}
return Result{
DNSRewriteResult: &DNSRewriteResult{
Response: DNSRewriteResultResponse{
qtype: vals,
},
RCode: dns.RcodeSuccess,
},
Rules: rs,
Reason: RewrittenAutoHosts,
}, nil
}
// hostsRewrites returns values and rules matched by qt and host within hs.
func hostsRewrites(
qtype uint16,
host string,
hs hostsfile.Storage,
) (vals []rules.RRValue, rls []*ResultRule, matched bool) {
var isValidProto func(netip.Addr) (ok bool)
switch qtype {
case dns.TypeA:
isValidProto = netip.Addr.Is4
case dns.TypeAAAA:
isValidProto = netip.Addr.Is6
case dns.TypePTR:
addr, err := netutil.IPFromReversedAddr(host)
if err != nil {
log.Debug("filtering: failed to parse PTR record %q: %s", host, err)
return nil, nil, false
}
names := hs.ByAddr(addr)
for _, name := range names {
vals = append(vals, name)
rls = append(rls, &ResultRule{
Text: fmt.Sprintf("%s %s", addr, name),
FilterListID: SysHostsListID,
})
}
return vals, rls, len(names) > 0
default:
log.Debug("filtering: unsupported qtype %d", qtype)
return nil, nil, false
}
addrs := hs.ByName(host)
for _, addr := range addrs {
if isValidProto(addr) {
vals = append(vals, addr)
}
rls = append(rls, &ResultRule{
Text: fmt.Sprintf("%s %s", addr, host),
FilterListID: SysHostsListID,
})
}
return vals, rls, len(addrs) > 0
}

View File

@@ -0,0 +1,191 @@
package filtering
import (
"fmt"
"net/netip"
"testing"
"testing/fstest"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
addrv4 := netip.MustParseAddr("1.2.3.4")
addrv6 := netip.MustParseAddr("::1")
addrMapped := netip.MustParseAddr("::ffff:1.2.3.4")
addrv4Dup := netip.MustParseAddr("4.3.2.1")
data := fmt.Sprintf(
""+
"%[1]s v4.host.example\n"+
"%[2]s v6.host.example\n"+
"%[3]s mapped.host.example\n"+
"%[4]s v4.host.with-dup\n"+
"%[4]s v4.host.with-dup\n",
addrv4,
addrv6,
addrMapped,
addrv4Dup,
)
files := fstest.MapFS{
"hosts": &fstest.MapFile{
Data: []byte(data),
},
}
watcher := &aghtest.FSWatcher{
OnEvents: func() (e <-chan struct{}) { return nil },
OnAdd: func(name string) (err error) { return nil },
OnClose: func() (err error) { return nil },
}
hc, err := aghnet.NewHostsContainer(files, watcher, "hosts")
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, hc.Close)
conf := &Config{
EtcHosts: hc,
}
f, err := New(conf, nil)
require.NoError(t, err)
setts := &Settings{
FilteringEnabled: true,
}
testCases := []struct {
name string
host string
wantRules []*ResultRule
wantResps []rules.RRValue
dtyp uint16
}{{
name: "v4",
host: "v4.host.example",
dtyp: dns.TypeA,
wantRules: []*ResultRule{{
Text: "1.2.3.4 v4.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{addrv4},
}, {
name: "v6",
host: "v6.host.example",
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: "::1 v6.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{addrv6},
}, {
name: "mapped",
host: "mapped.host.example",
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: "::ffff:1.2.3.4 mapped.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{addrMapped},
}, {
name: "ptr",
host: "4.3.2.1.in-addr.arpa",
dtyp: dns.TypePTR,
wantRules: []*ResultRule{{
Text: "1.2.3.4 v4.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{"v4.host.example"},
}, {
name: "ptr-mapped",
host: "4.0.3.0.2.0.1.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
dtyp: dns.TypePTR,
wantRules: []*ResultRule{{
Text: "::ffff:1.2.3.4 mapped.host.example",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{"mapped.host.example"},
}, {
name: "not_found_v4",
host: "non.existent.example",
dtyp: dns.TypeA,
wantRules: nil,
wantResps: nil,
}, {
name: "not_found_v6",
host: "non.existent.example",
dtyp: dns.TypeAAAA,
wantRules: nil,
wantResps: nil,
}, {
name: "not_found_ptr",
host: "4.3.2.2.in-addr.arpa",
dtyp: dns.TypePTR,
wantRules: nil,
wantResps: nil,
}, {
name: "v4_mismatch",
host: "v4.host.example",
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: fmt.Sprintf("%s v4.host.example", addrv4),
FilterListID: SysHostsListID,
}},
wantResps: nil,
}, {
name: "v6_mismatch",
host: "v6.host.example",
dtyp: dns.TypeA,
wantRules: []*ResultRule{{
Text: fmt.Sprintf("%s v6.host.example", addrv6),
FilterListID: SysHostsListID,
}},
wantResps: nil,
}, {
name: "wrong_ptr",
host: "4.3.2.1.ip6.arpa",
dtyp: dns.TypePTR,
wantRules: nil,
wantResps: nil,
}, {
name: "unsupported_type",
host: "v4.host.example",
dtyp: dns.TypeCNAME,
wantRules: nil,
wantResps: nil,
}, {
name: "v4_dup",
host: "v4.host.with-dup",
dtyp: dns.TypeA,
wantRules: []*ResultRule{{
Text: "4.3.2.1 v4.host.with-dup",
FilterListID: SysHostsListID,
}},
wantResps: []rules.RRValue{addrv4Dup},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var res Result
res, err = f.CheckHost(tc.host, tc.dtyp, setts)
require.NoError(t, err)
if len(tc.wantRules) == 0 {
assert.Empty(t, res.Rules)
assert.Nil(t, res.DNSRewriteResult)
return
}
require.NotNil(t, res.DNSRewriteResult)
require.Contains(t, res.DNSRewriteResult.Response, tc.dtyp)
assert.Equal(t, tc.wantResps, res.DNSRewriteResult.Response[tc.dtyp])
assert.Equal(t, tc.wantRules, res.Rules)
})
}
}

View File

@@ -24,23 +24,25 @@ func validateFilterURL(urlStr string) (err error) {
if filepath.IsAbs(urlStr) {
_, err = os.Stat(urlStr)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
return nil
// Don't wrap the error since it's informative enough as is.
return err
}
u, err := url.ParseRequestURI(urlStr)
if err != nil {
// 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 {
}
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}),
Err: fmt.Errorf("only %v allowed", []string{
aghhttp.SchemeHTTP,
aghhttp.SchemeHTTPS,
}),
}
}

View File

@@ -0,0 +1,338 @@
package rulelist
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/ioutil"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/c2h5oh/datasize"
)
// Filter contains information about a single rule-list filter.
//
// TODO(a.garipov): Use.
type Filter struct {
// url is the URL of this rule list. Supported schemes are:
// - http
// - https
// - file
url *url.URL
// ruleList is the last successfully compiled [filterlist.RuleList].
ruleList filterlist.RuleList
// updated is the time of the last successful update.
updated time.Time
// name is the human-readable name of this rule-list filter.
name string
// uid is the unique ID of this rule-list filter.
uid UID
// urlFilterID is used for working with package urlfilter.
urlFilterID URLFilterID
// rulesCount contains the number of rules in this rule-list filter.
rulesCount int
// checksum is a CRC32 hash used to quickly check if the rules within a list
// file have changed.
checksum uint32
// enabled, if true, means that this rule-list filter is used for filtering.
//
// TODO(a.garipov): Take into account.
enabled bool
}
// FilterConfig contains the configuration for a [Filter].
type FilterConfig struct {
// URL is the URL of this rule-list filter. Supported schemes are:
// - http
// - https
// - file
URL *url.URL
// Name is the human-readable name of this rule-list filter. If not set, it
// is either taken from the rule-list data or generated synthetically from
// the UID.
Name string
// UID is the unique ID of this rule-list filter.
UID UID
// URLFilterID is used for working with package urlfilter.
URLFilterID URLFilterID
// Enabled, if true, means that this rule-list filter is used for filtering.
Enabled bool
}
// NewFilter creates a new rule-list filter. The filter is not refreshed, so a
// refresh should be performed before use.
func NewFilter(c *FilterConfig) (f *Filter, err error) {
if c.URL == nil {
return nil, errors.Error("no url")
}
switch s := c.URL.Scheme; s {
case "http", "https", "file":
// Go on.
default:
return nil, fmt.Errorf("bad url scheme: %q", s)
}
return &Filter{
url: c.URL,
name: c.Name,
uid: c.UID,
urlFilterID: c.URLFilterID,
enabled: c.Enabled,
}, nil
}
// Refresh updates the data in the rule-list filter. parseBuf is the initial
// buffer used to parse information from the data. cli and maxSize are only
// used when f is a URL-based list.
func (f *Filter) Refresh(
ctx context.Context,
parseBuf []byte,
cli *http.Client,
cacheDir string,
maxSize datasize.ByteSize,
) (parseRes *ParseResult, err error) {
cachePath := filepath.Join(cacheDir, f.uid.String()+".txt")
switch s := f.url.Scheme; s {
case "http", "https":
parseRes, err = f.setFromHTTP(ctx, parseBuf, cli, cachePath, maxSize.Bytes())
case "file":
parseRes, err = f.setFromFile(parseBuf, f.url.Path, cachePath)
default:
// Since the URL has been prevalidated in New, consider this a
// programmer error.
panic(fmt.Errorf("bad url scheme: %q", s))
}
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
if f.checksum != parseRes.Checksum {
f.checksum = parseRes.Checksum
f.rulesCount = parseRes.RulesCount
f.setName(parseRes.Title)
f.updated = time.Now()
}
return parseRes, nil
}
// setFromHTTP sets the rule-list filter's data from its URL. It also caches
// the data into a file.
func (f *Filter) setFromHTTP(
ctx context.Context,
parseBuf []byte,
cli *http.Client,
cachePath string,
maxSize uint64,
) (parseRes *ParseResult, err error) {
defer func() { err = errors.Annotate(err, "setting from http: %w") }()
text, parseRes, err := f.readFromHTTP(ctx, parseBuf, cli, cachePath, maxSize)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
// TODO(a.garipov): Add filterlist.BytesRuleList.
f.ruleList = &filterlist.StringRuleList{
ID: f.urlFilterID,
RulesText: text,
IgnoreCosmetic: true,
}
return parseRes, nil
}
// readFromHTTP reads the data from the rule-list filter's URL into the cache
// file as well as returns it as a string. The data is filtered through a
// parser and so is free from comments, unnecessary whitespace, etc.
func (f *Filter) readFromHTTP(
ctx context.Context,
parseBuf []byte,
cli *http.Client,
cachePath string,
maxSize uint64,
) (text string, parseRes *ParseResult, err error) {
urlStr := f.url.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return "", nil, fmt.Errorf("making request for http url %q: %w", urlStr, err)
}
resp, err := cli.Do(req)
if err != nil {
return "", nil, fmt.Errorf("requesting from http url: %w", err)
}
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
// TODO(a.garipov): Use [agdhttp.CheckStatus] when it's moved to golibs.
if resp.StatusCode != http.StatusOK {
return "", nil, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
}
fltFile, err := aghrenameio.NewPendingFile(cachePath, 0o644)
if err != nil {
return "", nil, fmt.Errorf("creating temp file: %w", err)
}
defer func() { err = aghrenameio.WithDeferredCleanup(err, fltFile) }()
buf := &bytes.Buffer{}
mw := io.MultiWriter(buf, fltFile)
parser := NewParser()
httpBody := ioutil.LimitReader(resp.Body, maxSize)
parseRes, err = parser.Parse(mw, httpBody, parseBuf)
if err != nil {
return "", nil, fmt.Errorf("parsing response from http url %q: %w", urlStr, err)
}
return buf.String(), parseRes, nil
}
// setName sets the title using either the already-present name, the given title
// from the rule-list data, or a synthetic name.
func (f *Filter) setName(title string) {
if f.name != "" {
return
}
if title != "" {
f.name = title
return
}
f.name = fmt.Sprintf("List %s", f.uid)
}
// setFromFile sets the rule-list filter's data from a file path. It also
// caches the data into a file.
//
// TODO(a.garipov): Retest on Windows once rule-list updater is committed. See
// if calling Close is necessary here.
func (f *Filter) setFromFile(
parseBuf []byte,
filePath string,
cachePath string,
) (parseRes *ParseResult, err error) {
defer func() { err = errors.Annotate(err, "setting from file: %w") }()
parseRes, err = parseIntoCache(parseBuf, filePath, cachePath)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
err = f.Close()
if err != nil {
return nil, fmt.Errorf("closing old rule list: %w", err)
}
rl, err := filterlist.NewFileRuleList(f.urlFilterID, cachePath, true)
if err != nil {
return nil, fmt.Errorf("opening new rule list: %w", err)
}
f.ruleList = rl
return parseRes, nil
}
// parseIntoCache copies the relevant the data from filePath into cachePath
// while also parsing it.
func parseIntoCache(
parseBuf []byte,
filePath string,
cachePath string,
) (parseRes *ParseResult, err error) {
tmpFile, err := aghrenameio.NewPendingFile(cachePath, 0o644)
if err != nil {
return nil, fmt.Errorf("creating temp file: %w", err)
}
defer func() { err = aghrenameio.WithDeferredCleanup(err, tmpFile) }()
// #nosec G304 -- Assume that cachePath is always cacheDir joined with a
// uid using [filepath.Join].
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("opening src file: %w", err)
}
defer func() { err = errors.WithDeferred(err, f.Close()) }()
parser := NewParser()
parseRes, err = parser.Parse(tmpFile, f, parseBuf)
if err != nil {
return nil, fmt.Errorf("copying src file: %w", err)
}
return parseRes, nil
}
// Close closes the underlying rule list.
func (f *Filter) Close() (err error) {
if f.ruleList == nil {
return nil
}
return f.ruleList.Close()
}
// filterUpdate represents a single ongoing rule-list filter update.
//
//lint:ignore U1000 TODO(a.garipov): Use.
type filterUpdate struct {
httpCli *http.Client
cacheDir string
name string
parseBuf []byte
maxSize datasize.ByteSize
}
// process runs an update of a single rule-list.
func (u *filterUpdate) process(ctx context.Context, f *Filter) (err error) {
prevChecksum := f.checksum
parseRes, err := f.Refresh(ctx, u.parseBuf, u.httpCli, u.cacheDir, u.maxSize)
if err != nil {
return fmt.Errorf("updating %s: %w", f.uid, err)
}
if prevChecksum == parseRes.Checksum {
log.Info("filtering: filter %q: filter %q: no change", u.name, f.uid)
return nil
}
log.Info(
"filtering: updated filter %q: filter %q: %d bytes, %d rules",
u.name,
f.uid,
parseRes.BytesWritten,
parseRes.RulesCount,
)
return nil
}

View File

@@ -0,0 +1,107 @@
package rulelist_test
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFilter_Refresh(t *testing.T) {
cacheDir := t.TempDir()
uid := rulelist.MustNewUID()
initialFile := filepath.Join(cacheDir, "initial.txt")
initialData := []byte(
testRuleTextTitle +
testRuleTextBlocked,
)
writeErr := os.WriteFile(initialFile, initialData, 0o644)
require.NoError(t, writeErr)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
pt := testutil.PanicT{}
_, err := io.WriteString(w, testRuleTextTitle+testRuleTextBlocked)
require.NoError(pt, err)
}))
srvURL, urlErr := url.Parse(srv.URL)
require.NoError(t, urlErr)
testCases := []struct {
url *url.URL
name string
wantNewErrMsg string
}{{
url: nil,
name: "nil_url",
wantNewErrMsg: "no url",
}, {
url: &url.URL{
Scheme: "ftp",
},
name: "bad_scheme",
wantNewErrMsg: `bad url scheme: "ftp"`,
}, {
name: "file",
url: &url.URL{
Scheme: "file",
Path: initialFile,
},
wantNewErrMsg: "",
}, {
name: "http",
url: srvURL,
wantNewErrMsg: "",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f, err := rulelist.NewFilter(&rulelist.FilterConfig{
URL: tc.url,
Name: tc.name,
UID: uid,
URLFilterID: testURLFilterID,
Enabled: true,
})
if tc.wantNewErrMsg != "" {
assert.EqualError(t, err, tc.wantNewErrMsg)
return
}
testutil.CleanupAndRequireSuccess(t, f.Close)
require.NotNil(t, f)
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
t.Cleanup(cancel)
buf := make([]byte, rulelist.DefaultRuleBufSize)
cli := &http.Client{
Timeout: testTimeout,
}
res, err := f.Refresh(ctx, buf, cli, cacheDir, rulelist.DefaultMaxRuleListSize)
require.NoError(t, err)
assert.Equal(t, testTitle, res.Title)
assert.Equal(t, len(testRuleTextBlocked), res.BytesWritten)
assert.Equal(t, 1, res.RulesCount)
// Check that the cached file exists.
_, err = os.Stat(filepath.Join(cacheDir, uid.String()+".txt"))
require.NoError(t, err)
})
}
}

View File

@@ -69,12 +69,12 @@ func TestParser_Parse(t *testing.T) {
wantWritten: len(testRuleTextBlocked) + len(testRuleTextHTML),
}, {
name: "title",
in: "! Title: Test Title \n" +
in: testRuleTextTitle +
"! Title: Bad, Ignored Title\n" +
testRuleTextBlocked,
wantDst: testRuleTextBlocked,
wantErrMsg: "",
wantTitle: "Test Title",
wantTitle: testTitle,
wantRulesNum: 1,
wantWritten: len(testRuleTextBlocked),
}, {
@@ -87,14 +87,14 @@ func TestParser_Parse(t *testing.T) {
wantWritten: len(testRuleTextCosmetic),
}, {
name: "bad_char",
in: "! Title: Test Title \n" +
in: testRuleTextTitle +
testRuleTextBlocked +
">>>\x7F<<<",
wantDst: testRuleTextBlocked,
wantErrMsg: "line 3: " +
"character 4: " +
"likely binary character '\\x7f'",
wantTitle: "Test Title",
wantTitle: testTitle,
wantRulesNum: 1,
wantWritten: len(testRuleTextBlocked),
}, {

View File

@@ -1,9 +1,55 @@
// Package rulelist contains the implementation of the standard rule-list
// filter that wraps an urlfilter filtering-engine.
//
// TODO(a.garipov): Expand.
// TODO(a.garipov): Add a new update worker.
package rulelist
import (
"fmt"
"github.com/c2h5oh/datasize"
"github.com/google/uuid"
)
// DefaultRuleBufSize is the default length of a buffer used to read a line with
// a filtering rule, in bytes.
//
// TODO(a.garipov): Consider using [datasize.ByteSize]. It is currently only
// used as an int.
const DefaultRuleBufSize = 1024
// DefaultMaxRuleListSize is the default maximum filtering-rule list size.
const DefaultMaxRuleListSize = 64 * datasize.MB
// URLFilterID is a semantic type-alias for IDs used for working with package
// urlfilter.
type URLFilterID = int
// UID is the type for the unique IDs of filtering-rule lists.
type UID uuid.UUID
// NewUID returns a new filtering-rule list UID. Any error returned is an error
// from the cryptographic randomness reader.
func NewUID() (uid UID, err error) {
uuidv7, err := uuid.NewV7()
return UID(uuidv7), err
}
// MustNewUID is a wrapper around [NewUID] that panics if there is an error.
func MustNewUID() (uid UID) {
uid, err := NewUID()
if err != nil {
panic(fmt.Errorf("unexpected uuidv7 error: %w", err))
}
return uid
}
// type check
var _ fmt.Stringer = UID{}
// String implements the [fmt.Stringer] interface for UID.
func (id UID) String() (s string) {
return uuid.UUID(id).String()
}

View File

@@ -1,16 +1,34 @@
package rulelist_test
import "time"
import (
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/testutil"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second
// Common texts for tests.
// testURLFilterID is the common [rulelist.URLFilterID] for tests.
const testURLFilterID rulelist.URLFilterID = 1
// testTitle is the common title for tests.
const testTitle = "Test Title"
// Common rule texts for tests.
const (
testRuleTextBadTab = "||bad-tab-and-comment.example^\t# A comment.\n"
testRuleTextBlocked = "||blocked.example^\n"
testRuleTextBlocked2 = "||blocked-2.example^\n"
testRuleTextEtcHostsTab = "0.0.0.0 tab..example^\t# A comment.\n"
testRuleTextHTML = "<!DOCTYPE html>\n"
testRuleTextTitle = "! Title: " + testTitle + " \n"
// testRuleTextCosmetic is a cosmetic rule with a zero-width non-joiner.
//

View File

@@ -226,7 +226,8 @@ func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRe
// empty result is converted into a NODATA response.
//
// TODO(a.garipov): Use the main rewrite result mechanism used in
// [dnsforward.Server.filterDNSRequest].
// [dnsforward.Server.filterDNSRequest]. Now we resolve IPs for CNAME to save
// them in the safe search cache.
func (ss *Default) newResult(
rewrite *rules.DNSRewrite,
qtype rules.RRType,
@@ -255,6 +256,8 @@ func (ss *Default) newResult(
return res, nil
}
res.CanonName = host
ss.log(log.DEBUG, "resolving %q", host)
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)

View File

@@ -12,6 +12,15 @@ type blockedService struct {
// blockedServices contains raw blocked service data.
var blockedServices = []blockedService{{
ID: "4chan",
Name: "4chan",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M33.25 2C31.311 2 29 3.724 29 11.928c0 4.614 1.237 8.92 1.29 9.101l.265.909.922-.215a32.132 32.132 0 0 0 5.472-1.907C39.37 18.711 45 15.614 45 10.75 45 8.098 43.789 5 40.375 5c-.996 0-1.807.356-2.387.736C36.868 4.038 35.13 2 33.25 2zM11.852 4C8.297 4 6 5.956 6 8.984c0 .975.355 1.752.746 2.325C5.044 12.389 3 14.129 3 16.25c0 1.428.97 4.75 9.943 4.75 5.104 0 8.958-1.248 9.12-1.3l.837-.276-.17-.865c-.042-.215-1.054-5.3-3.75-9.905C18.038 7.044 15.335 4 11.852 4zm8.22 24.057-.836.228c-.273.075-2.742.774-5.336 2.336-3.184 1.916-6.9 5.436-6.9 9.047C7 43.987 9.755 46 11.617 46a4.894 4.894 0 0 0 2.791-.877C15.383 46.262 17.144 48 19.316 48 22.957 48 23 41.027 23 40.957c0-6.061-2.477-11.86-2.582-12.103l-.346-.797zm16.67.943c-5.242 0-8.636 1.001-8.777 1.043l-.83.248.127.855c.02.138.518 3.416 1.88 6.627C31.395 43.078 34.483 46 37.845 46 42.704 46 44 43.161 44 41.484a4.94 4.94 0 0 0-.768-2.611C44.666 37.885 47 35.971 47 33.984 47 29.916 39.403 29 36.742 29z\"/></svg>"),
Rules: []string{
"||4cdn.org^",
"||4chan.org^",
"||4channel.org^",
},
}, {
ID: "500px",
Name: "500px",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M 5 14 L 2.5 26 L 6.800781 26 C 6.800781 26 7.699219 24.300781 10.199219 24.300781 C 12.699219 24.300781 14 26.199219 14 28.300781 C 14 30.402344 12.5 32.800781 10.199219 32.800781 C 7.898438 32.800781 6.5 30.398438 6.5 29 L 2 29 C 2 30.199219 3 36 10.199219 36 C 15.15625 36 17.417969 33.121094 18.015625 31.898438 C 19.386719 34.34375 21.992188 36 24.984375 36 C 27.253906 36 29.777344 34.808594 32.5 32.453125 C 35.222656 34.808594 37.746094 36 40.015625 36 C 44.417969 36 48 32.410156 48 28 C 48 23.589844 44.417969 20 40.015625 20 C 37.746094 20 35.222656 21.191406 32.5 23.546875 C 29.777344 21.191406 27.253906 20 24.984375 20 C 21.832031 20 19.105469 21.847656 17.8125 24.511719 C 17.113281 23.382813 15.414063 21 11.902344 21 C 8.101563 21 7.300781 22.597656 7.300781 22.597656 C 7.300781 22.597656 7.699219 21.300781 8.300781 18 L 17 18 L 17 14 Z M 24.984375 25 C 25.453125 25 26.800781 25.226563 29.230469 27.328125 L 30.011719 28 L 29.230469 28.671875 C 26.800781 30.773438 25.453125 31 24.984375 31 C 23.339844 31 22 29.652344 22 28 C 22 26.347656 23.339844 25 24.984375 25 Z M 40.015625 25 C 41.660156 25 43 26.347656 43 28 C 43 29.652344 41.660156 31 40.015625 31 C 39.546875 31 38.199219 30.773438 35.769531 28.671875 L 34.988281 28 L 35.769531 27.328125 C 38.199219 25.226563 39.546875 25 40.015625 25 Z\"/></svg>"),
@@ -256,6 +265,41 @@ var blockedServices = []blockedService{{
"||z.cn^",
"||zappos^",
},
}, {
ID: "amazon_streaming",
Name: "Amazon Streaming",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 32 32\"><path d=\"M16.2,4c-3.3,0-6.9,1.2-7.7,5.3C8.4,9.7,8.7,10,9,10l3.3,0.3c0.3,0,0.6-0.3,0.6-0.6c0.3-1.4,1.5-2.1,2.8-2.1c0.7,0,1.5,0.3,1.9,0.9c0.5,0.7,0.4,1.7,0.4,2.5v0.5c-2,0.2-4.6,0.4-6.5,1.2c-2.2,0.9-3.7,2.8-3.7,5.7c0,3.6,2.3,5.4,5.2,5.4c2.5,0,3.8-0.6,5.7-2.5c0.6,0.9,0.9,1.4,2,2.3c0.3,0.1,0.6,0.1,0.8-0.1v0c0.7-0.6,2-1.7,2.7-2.3c0.3-0.2,0.2-0.6,0-0.9c-0.6-0.9-1.3-1.6-1.3-3.2v-5.4c0-2.3,0.2-4.4-1.5-6C20.1,4.4,17.9,4,16.2,4z M17.1,14.3c0.3,0,0.6,0,0.9,0v0.8c0,1.3,0.1,2.5-0.6,3.7c-0.5,1-1.4,1.6-2.4,1.6c-1.3,0-2.1-1-2.1-2.5C12.9,15.2,14.9,14.5,17.1,14.3z M26.7,22.4c-0.9,0-1.9,0.2-2.7,0.8c-0.2,0.2-0.2,0.4,0.1,0.4c0.9-0.1,2.8-0.4,3.2,0.1s-0.4,2.3-0.7,3.1c-0.1,0.2,0.1,0.3,0.3,0.2c1.5-1.2,1.9-3.8,1.6-4.2C28.3,22.5,27.6,22.4,26.7,22.4z M3.7,22.8c-0.2,0-0.3,0.3-0.1,0.4c3.3,3,7.6,4.7,12.4,4.7c3.4,0,7.4-1.1,10.2-3.1c0.5-0.3,0.1-0.9-0.4-0.7c-3.1,1.3-6.4,1.9-9.5,1.9c-4.5,0-8.8-1.2-12.4-3.3C3.8,22.9,3.7,22.8,3.7,22.8z\" /></svg>"),
Rules: []string{
"||aiv-delivery.net^",
"||amazonmusic.com^",
"||amazonprimevideo.cn^",
"||amazonprimevideo.com.cn^",
"||amazonprimevideos.com^",
"||amazonvideo.cc^",
"||amazonvideo.com^",
"||amazonvideodirect.com^",
"||atv-ext-eu.amazon.com^",
"||atv-ext-fe.amazon.com^",
"||atv-ext.amazon.com^",
"||atv-ps-eu.amazon.co.uk^",
"||atv-ps-eu.amazon.com^",
"||atv-ps-fe.amazon.co.jp^",
"||atv-ps-fe.amazon.com^",
"||atv-ps.amazon.com^",
"||av-eu.amazon.com^",
"||av-na.amazon.com^",
"||music.a2z.com^",
"||music.amazon.co.uk^",
"||music.amazon.com^",
"||music.amazon.in^",
"||prime-video.com^",
"||primevideo.cc^",
"||primevideo.com^",
"||primevideo.info^",
"||primevideo.org^",
"||primevideo.tv^",
"||video.a2z.com^",
},
}, {
ID: "amino",
Name: "Amino",
@@ -375,6 +419,7 @@ var blockedServices = []blockedService{{
"||biliapi.com^",
"||biliapi.net^",
"||bilibili.cc^",
"||bilibili.cn^",
"||bilibili.com^",
"||bilibili.net^",
"||bilibili.tv^",
@@ -392,6 +437,8 @@ var blockedServices = []blockedService{{
"||biligame.com^",
"||biligame.net^",
"||biligo.com^",
"||biliimg.com^",
"||biliintl.com^",
"||bilivideo.cn^",
"||bilivideo.com^",
"||bilivideo.net^",
@@ -583,6 +630,14 @@ var blockedServices = []blockedService{{
"||discordstatus.com^",
"||watchanimeattheoffice.com^",
},
}, {
ID: "discoveryplus",
Name: "Discovery+",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"166 -24 320 320\"><path d=\"M346.4.1h-98.2v71.5a101 101 0 0 0-81.7 98.9c0 58.6 48.2 101 101.2 101h78.7c78.5 0 138.5-59.9 138.5-135.7C485 62.2 424.9.1 346.4.1Zm0 263.5h-78.7a93.3 93.3 0 0 1-11.5-185.7V8.1h90.2c68.5 0 130.5 52.9 130.5 127.7.1 77.2-61.9 127.8-130.5 127.8Z\"/><path d=\"M345.8 22h-77.3v56c45.5 0 92 37.7 92 93.2S315.3 251 315.3 251h30.5c61.5 0 117.7-45.1 117.7-114.4C463.5 66.1 403.1 22 345.8 22Z\"/><path d=\"M347.5 170a80 80 0 0 1-80 80 80 80 0 0 1-80-80 80 80 0 0 1 80-80 80 80 0 0 1 80 80z\"/></svg>"),
Rules: []string{
"||disco-api.com^",
"||discoveryplus.com^",
},
}, {
ID: "disneyplus",
Name: "Disney+",
@@ -1449,6 +1504,16 @@ var blockedServices = []blockedService{{
"||flickrpro.com^",
"||staticflickr.com^",
},
}, {
ID: "globoplay",
Name: "Globoplay",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"30 -5 90 90\"><path d=\"M36.45 18.035a8.498 8.498 0 0 0-6.298 7.33 8.23 8.23 0 0 0 3.298 8.27 8.685 8.685 0 0 0 4.952 1.635 5.696 5.696 0 0 0 4.497-1.714c.06 2.704-1.139 4.685-4.319 4.784a10.012 10.012 0 0 1-5.556-1.535c-.901-.535-1.337-.376-1.793.535-.257.505-.505 1.04-.782 1.555-.397.723-.258 1.178.465 1.773a11.886 11.886 0 0 0 7.23 2.218c8.063 0 9.559-5.962 9.559-10.707V19.67c.02-1.753.079-1.654-2.457-1.654a3.16 3.16 0 0 0-1.584.238c-.327.258-.387.753-.387 1.535a6.601 6.601 0 0 0-6.825-1.753m2.469 13.153c-2.486-.02-4.319-1.654-4.319-4.556a4.15 4.15 0 0 1 4.338-4.467 4.19 4.19 0 0 1 4.16 4.507h-.02a4.16 4.16 0 0 1-4.144 4.516h-.015m76.025 14.606c-.644 1.872-1.337 3.705-1.981 5.557-.476 1.347-1.05 2.843-1.555 4.319 0 0-2.645-6.795-3.863-9.905a1.11 1.11 0 0 0-1.229-.852h-2.753c-1.436 0-1.496.079-.911 1.416 2.07 4.755 6.606 15.095 6.606 15.095-.387.923-.851 1.81-1.386 2.655a2.042 2.042 0 0 1-2.477.99c-1.674-.396-1.674-.376-2.486 1.199a8.951 8.951 0 0 0-.336.594c-.288.505-.466 1.178.138 1.496a5.717 5.717 0 0 0 3.387 1.137h.071c.088-.001.178-.004.266-.008 3.19 0 5.131-2.892 5.943-4.873 1.288-3.23 2.952-7.191 4.259-10.44 1.06-2.665 2.16-5.3 3.15-7.924.436-1.08.238-1.397-.931-1.397-.862 0-1.714.06-2.555 0a1.168 1.168 0 0 0-1.357.941M76.237 12.072V34.1c-.016.288.046.575.178.832.337.515 3.675.554 3.962 0 .238-.308.119-1.397.238-1.298a7.696 7.696 0 0 0 11.153-.892c2.703-2.931 2.684-8.775.237-11.746a8.19 8.19 0 0 0-6.517-3.25 6.754 6.754 0 0 0-4.507 1.813c-.02-.08-.06-5.28-.079-7.408 0-.882-.267-1.298-1.258-1.298-.733.06-1.317.02-2.17 0h-.065c-.806 0-1.172.36-1.172 1.219m8.486 19.155a4.25 4.25 0 0 1-3.92-4.555c.06-2.734 1.872-4.487 4.24-4.487a4.22 4.22 0 0 1 4.24 4.546 4.249 4.249 0 0 1-4.237 4.508 4.33 4.33 0 0 1-.323-.012m-23.521 15.39c0-1.297.079-1.693-1.278-1.693h-1.872c-.773-.11-1.05.267-1.05.99v22.098c0 .713.218 1.07.99 1.07h1.773c1.714 0 1.714 0 1.714-1.655v-6.933c.475.297.723.495 1.05.674a6.815 6.815 0 0 0 7.101.386 9.033 9.033 0 0 0 4.576-10.281c-.853-3.919-4.351-6.738-7.994-6.737-1.726 0-3.485.633-5.01 2.081m4.445 11.341a4.328 4.328 0 0 1-4.249-4.596 4.22 4.22 0 0 1 4.447-4.467c2.684.1 4.081 1.952 4.081 4.507a4.23 4.23 0 0 1-4.215 4.557h-.064M87.29 45.776a9.231 9.231 0 0 0-.753 14.738 7.084 7.084 0 0 0 5.2 1.832 6.24 6.24 0 0 0 4.557-2.06c0 1.734.317 1.734 1.892 1.734h1.377c.901.079 1.258-.377 1.258-1.209V46.033c0-.762-.278-1.258-1.16-1.159h-1.574c-1.753-.02-1.535 0-1.753 1.595l-.773-.495a6.925 6.925 0 0 0-4.269-1.47c-1.399 0-2.8.423-4.001 1.272m.564 7.626v-.04a4.279 4.279 0 0 1 4.16-4.487l.154.005a4.348 4.348 0 0 1 4.145 4.542 4.279 4.279 0 0 1-4.26 4.596l-.105.001c-2.372 0-4.094-1.972-4.094-4.617m-31.291-26.91a8.855 8.855 0 0 0 9.053 9.217 8.855 8.855 0 0 0 8.647-9.058c.001-.067.002-.133.001-.2a8.736 8.736 0 0 0-8.787-8.684h-.071a8.846 8.846 0 0 0-8.843 8.726m8.63 4.806a4.487 4.487 0 0 1-4.143-4.807c.002-.069.004-.138.01-.207a4.299 4.299 0 0 1 8.579.564v.06a4.358 4.358 0 0 1-4.32 4.397 4.08 4.08 0 0 1-.125-.008m30.527-4.665a8.816 8.816 0 1 0 17.63.258 8.816 8.816 0 0 0-8.686-8.945 8.817 8.817 0 0 0-8.944 8.686m4.478.377a4.378 4.378 0 0 1 4.17-4.863 4.259 4.259 0 0 1 4.437 4.487v.02a4.338 4.338 0 0 1-4.16 4.655h-.021a4.428 4.428 0 0 1-4.426-4.299M77.93 37.762c-1.228.02-1.397.198-1.397 1.436v21.514c0 .89.307 1.346 1.298 1.287.722-.04 1.446-.04 2.169 0 .822.04 1.139-.297 1.139-1.149V49.895c0-3.644-.02-7.31 0-10.974 0-.822-.297-1.159-1.139-1.159h-2.07ZM50.846 10.505c-.669.004-.913.505-.913 1.408v11.252c-.02 1.09-.02 2.199 0 3.21v7.923c-.02 1.317.812 1.694 1.713 1.317.456-.168 1.07-.485 1.377-.623 1.506-.674 1.446-1.21 1.446-2.427.06-6.25.06-13.045.06-19.126 0-1.595-.496-1.753-2.407-2.586-.528-.233-.942-.346-1.26-.348h-.016Z\"/></svg>"),
Rules: []string{
"||cloud-jarvis.globo.com^",
"||globoplay.com.br^",
"||globoplay.com^",
"||globoplay.globo.com^",
},
}, {
ID: "gog",
Name: "GOG",
@@ -1475,6 +1540,7 @@ var blockedServices = []blockedService{{
"||hbomax.com^",
"||hbomaxcdn.com^",
"||hbonow.com^",
"||max.com^",
"||maxgo.com^",
},
}, {
@@ -1602,6 +1668,12 @@ var blockedServices = []blockedService{{
Rules: []string{
"||iq.com^",
"||iqiyi.com^",
"||iqiyipic.com^",
"||pps.tv^",
"||ppsimg.com^",
"||qiyi.com^",
"||qiyipic.com^",
"||qy.net^",
},
}, {
ID: "kakaotalk",
@@ -2140,6 +2212,16 @@ var blockedServices = []blockedService{{
"||rockstargames.com^",
"||rsg.sc^",
},
}, {
ID: "samsung_tv_plus",
Name: "Samsung TV Plus",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"121 -91 672 672\"><path d=\"m710.504 89.711.004 38.892c0 .073.021.14.021.214V352.97c0 21.351-17.05 38.526-38.326 38.835l-.05.052h-.473c-.015 0-.028.004-.042.004h-96.769v31.034a24.11 24.11 0 0 1-24.162 24.163H368.046a24.11 24.11 0 0 1-24.162-24.163v-31.034H208.832v35.86c0 34.394 27.687 62.083 62.081 62.083h457.22c34.395 0 62.085-27.69 62.085-62.083V151.795c0-34.394-27.69-62.084-62.084-62.084zM185.028 0c-34.394 0-62.084 27.69-62.084 62.084V329.78c0 34.393 27.69 62.081 62.083 62.081h23.804V128.817c0-21.546 17.346-38.89 38.892-38.89h38.6l.176-.216h424.005V62.084C710.504 27.69 682.814 0 648.421 0Z\"/></svg>"),
Rules: []string{
"||internetat.tv^",
"||samsung.wurl.tv^",
"||samsungcloud.tv^",
"||samsungtvplus.com^",
},
}, {
ID: "shein",
Name: "Shein",
@@ -2328,7 +2410,6 @@ var blockedServices = []blockedService{{
Name: "TikTok",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M41 4H9C6.243 4 4 6.243 4 9v32c0 2.757 2.243 5 5 5h32c2.757 0 5-2.243 5-5V9c0-2.757-2.243-5-5-5zm-3.994 18.323a7.482 7.482 0 0 1-.69.035 7.492 7.492 0 0 1-6.269-3.388v11.537a8.527 8.527 0 1 1-8.527-8.527c.178 0 .352.016.527.027v4.202c-.175-.021-.347-.053-.527-.053a4.351 4.351 0 1 0 0 8.704c2.404 0 4.527-1.894 4.527-4.298l.042-19.594h4.016a7.488 7.488 0 0 0 6.901 6.685v4.67z\" /></svg>"),
Rules: []string{
"|p16-tiktokcdn-com.akamaized.net^",
"||amemv.com^",
"||bdurl.com^",
"||bytecdn.cn^",
@@ -2348,6 +2429,7 @@ var blockedServices = []blockedService{{
"||muscdn.com^",
"||musical.ly^",
"||p16-tiktok-*.ibyteimg.com^",
"||p16-tiktokcdn-com.akamaized.net^",
"||pstatp.com^",
"||snssdk.com^",
"||tiktok.com^",
@@ -2356,6 +2438,7 @@ var blockedServices = []blockedService{{
"||tiktokv.com^",
"||ttlivecdn.com.c.bytefcdn-oversea.com^",
"||ttlivecdn.com^",
"||v*.tiktokcdn-eu.com^",
},
}, {
ID: "tinder",
@@ -2580,6 +2663,7 @@ var blockedServices = []blockedService{{
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M35 22v2h1v-2h-1zm0 0v2h1v-2h-1zm9-18H6c-1.09 0-2 .91-2 2v38c0 1.09.91 2 2 2h38c1.09 0 2-.91 2-2V6c0-1.09-.91-2-2-2zM12 24c0 1.38-.19 5.89-2.61 6.24l-.28-1.98c.39-.19.89-2.14.89-4.26v-2h2v2zm3 6h-2V19h2v11zm2.29-.29c-1.2-1.2-1.29-4.73-1.29-5.78V22h2v1.93c0 1.91.34 3.99.71 4.36l-1.42 1.42zM22 31h-3l1-2h3l-1 2zm9 0h-7l1-2h2v-7h-2l-2.1 4.38h1.72l-1 2H21a1 1 0 0 1-.82-1.57L22 24h-2a1 1 0 0 1-.86-1.51l3-5 1.72 1.02L21.77 22H25v-2h6v2h-2v7h2v2zm9-2.5a2.5 2.5 0 0 1-2.5 2.5c-1.21 0-1.22-.86-1.45-2H38v-3h-3v5h-2v-5h-2v-2h2v-2h-1v-2h1v-1h2v1h1a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2.5zm0-6.5h-1v-1c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1zm-5 2h1v-2h-1v2zm0-2v2h1v-2h-1z\"/></svg>"),
Rules: []string{
"||xhscdn.com^",
"||xhscdn.net^",
"||xiaohongshu.com^",
},
}, {