all: sync with master
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
92
internal/filtering/hosts.go
Normal file
92
internal/filtering/hosts.go
Normal 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
|
||||
}
|
||||
191
internal/filtering/hosts_test.go
Normal file
191
internal/filtering/hosts_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
338
internal/filtering/rulelist/filter.go
Normal file
338
internal/filtering/rulelist/filter.go
Normal 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
|
||||
}
|
||||
107
internal/filtering/rulelist/filter_test.go
Normal file
107
internal/filtering/rulelist/filter_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}, {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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^",
|
||||
},
|
||||
}, {
|
||||
|
||||
Reference in New Issue
Block a user