Compare commits
5 Commits
5347-wildc
...
5799-allow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df209c99bd | ||
|
|
668155b367 | ||
|
|
9caf0d54c6 | ||
|
|
b72a3d01b8 | ||
|
|
0393e41096 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -23,8 +23,16 @@ See also the [v0.107.30 GitHub milestone][ms-v0.107.30].
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### Added
|
||||
|
||||
- The ability to edit rewrite rules via `PUT /control/rewrite/update` HTTP API
|
||||
([#1577]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Private networks are now ignored from allowed/blocked clients access lists.
|
||||
These addresses are always allowed ([#5799]).
|
||||
|
||||
- Unquoted IPv6 bind hosts with trailing colons erroneously considered
|
||||
unspecified addresses are now properly validated ([#5752]).
|
||||
|
||||
@@ -35,7 +43,9 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
||||
- Provided bootstrap servers are now used to resolve the hostnames of plain
|
||||
UDP/TCP upstream servers.
|
||||
|
||||
[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577
|
||||
[#5716]: https://github.com/AdguardTeam/AdGuardHome/issues/5716
|
||||
[#5799]: https://github.com/AdguardTeam/AdGuardHome/issues/5799
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
|
||||
2
go.mod
2
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/renameio v1.0.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||
github.com/kardianos/service v1.2.2
|
||||
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
||||
|
||||
2
go.sum
2
go.sum
@@ -67,6 +67,8 @@ github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb h1:6fDKEAXwe3rsfS4khW3EZ8kEqmSiV9szhMPcDrD+Y7Q=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230516061539-49801966e6cb/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
|
||||
@@ -304,7 +304,7 @@ func tryConn6(req *dhcpv6.Message, c net.PacketConn) (ok, next bool, err error)
|
||||
if !(response.Type() == dhcpv6.MessageTypeAdvertise &&
|
||||
msg.TransactionID == req.TransactionID &&
|
||||
rcid != nil &&
|
||||
cid.Equal(*rcid)) {
|
||||
cid.Equal(rcid)) {
|
||||
|
||||
log.Debug("dhcpv6: received message from server doesn't match our request")
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type v6Server struct {
|
||||
leasesLock sync.Mutex
|
||||
leases []*Lease
|
||||
ipAddrs [256]byte
|
||||
sid dhcpv6.Duid
|
||||
sid dhcpv6.DUID
|
||||
|
||||
ra raCtx // RA module
|
||||
|
||||
@@ -659,9 +659,8 @@ func (s *v6Server) Start() (err error) {
|
||||
return fmt.Errorf("validating interface %s: %w", iface.Name, err)
|
||||
}
|
||||
|
||||
s.sid = dhcpv6.Duid{
|
||||
Type: dhcpv6.DUID_LLT,
|
||||
HwType: iana.HWTypeEthernet,
|
||||
s.sid = &dhcpv6.DUIDLLT{
|
||||
HWType: iana.HWTypeEthernet,
|
||||
LinkLayerAddr: iface.HardwareAddr,
|
||||
Time: dhcpv6.GetTime(),
|
||||
}
|
||||
|
||||
@@ -121,9 +121,8 @@ func TestV6GetLease(t *testing.T) {
|
||||
|
||||
dnsAddr := net.ParseIP("2000::1")
|
||||
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
|
||||
s.sid = dhcpv6.Duid{
|
||||
Type: dhcpv6.DUID_LLT,
|
||||
HwType: iana.HWTypeEthernet,
|
||||
s.sid = &dhcpv6.DUIDLL{
|
||||
HWType: iana.HWTypeEthernet,
|
||||
LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
|
||||
@@ -216,9 +215,8 @@ func TestV6GetDynamicLease(t *testing.T) {
|
||||
|
||||
dnsAddr := net.ParseIP("2000::1")
|
||||
s.conf.dnsIPAddrs = []net.IP{dnsAddr}
|
||||
s.sid = dhcpv6.Duid{
|
||||
Type: dhcpv6.DUID_LLT,
|
||||
HwType: iana.HWTypeEthernet,
|
||||
s.sid = &dhcpv6.DUIDLL{
|
||||
HWType: iana.HWTypeEthernet,
|
||||
LinkLayerAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/AdguardTeam/urlfilter/filterlist"
|
||||
@@ -30,6 +31,9 @@ type accessManager struct {
|
||||
|
||||
blockedHostsEng *urlfilter.DNSEngine
|
||||
|
||||
// privateNets is the set of IP networks those are ignored by the manager.
|
||||
privateNets netutil.SubnetSet
|
||||
|
||||
// TODO(a.garipov): Create a type for a set of IP networks.
|
||||
allowedNets []netip.Prefix
|
||||
blockedNets []netip.Prefix
|
||||
@@ -64,13 +68,20 @@ func processAccessClients(
|
||||
}
|
||||
|
||||
// newAccessCtx creates a new accessCtx.
|
||||
func newAccessCtx(allowed, blocked, blockedHosts []string) (a *accessManager, err error) {
|
||||
func newAccessCtx(
|
||||
allowed []string,
|
||||
blocked []string,
|
||||
blockedHosts []string,
|
||||
privateNets netutil.SubnetSet,
|
||||
) (a *accessManager, err error) {
|
||||
a = &accessManager{
|
||||
allowedIPs: map[netip.Addr]unit{},
|
||||
blockedIPs: map[netip.Addr]unit{},
|
||||
|
||||
allowedClientIDs: stringutil.NewSet(),
|
||||
blockedClientIDs: stringutil.NewSet(),
|
||||
|
||||
privateNets: privateNets,
|
||||
}
|
||||
|
||||
err = processAccessClients(allowed, a.allowedIPs, &a.allowedNets, a.allowedClientIDs)
|
||||
@@ -138,9 +149,13 @@ func (a *accessManager) isBlockedHost(host string, qt rules.RRType) (ok bool) {
|
||||
return ok
|
||||
}
|
||||
|
||||
// isBlockedIP returns the status of the IP address blocking as well as the rule
|
||||
// that blocked it.
|
||||
// isBlockedIP returns the status of the IP address blocking as well as the
|
||||
// rule that blocked it. Addresses from private nets are always allowed.
|
||||
func (a *accessManager) isBlockedIP(ip netip.Addr) (blocked bool, rule string) {
|
||||
if a.privateNets != nil && a.privateNets.Contains(ip.AsSlice()) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
blocked = true
|
||||
ips := a.blockedIPs
|
||||
ipnets := a.blockedNets
|
||||
@@ -241,7 +256,7 @@ func (s *Server) handleAccessSet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var a *accessManager
|
||||
a, err = newAccessCtx(list.AllowedClients, list.DisallowedClients, list.BlockedHosts)
|
||||
a, err = newAccessCtx(list.AllowedClients, list.DisallowedClients, list.BlockedHosts, nil)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "creating access ctx: %s", err)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -14,12 +15,12 @@ func TestIsBlockedClientID(t *testing.T) {
|
||||
clientID := "client-1"
|
||||
clients := []string{clientID}
|
||||
|
||||
a, err := newAccessCtx(clients, nil, nil)
|
||||
a, err := newAccessCtx(clients, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, a.isBlockedClientID(clientID))
|
||||
|
||||
a, err = newAccessCtx(nil, clients, nil)
|
||||
a, err = newAccessCtx(nil, clients, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, a.isBlockedClientID(clientID))
|
||||
@@ -31,7 +32,7 @@ func TestIsBlockedHost(t *testing.T) {
|
||||
"*.host.com",
|
||||
"||host3.com^",
|
||||
"||*^$dnstype=HTTPS",
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
@@ -103,58 +104,104 @@ func TestIsBlockedHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBlockedIP(t *testing.T) {
|
||||
func TestAccessManager_IsBlockedIP_allow(t *testing.T) {
|
||||
clients := []string{
|
||||
"1.2.3.4",
|
||||
"5.6.7.8/24",
|
||||
}
|
||||
|
||||
allowCtx, err := newAccessCtx(clients, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
blockCtx, err := newAccessCtx(nil, clients, nil)
|
||||
privateNets := netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
||||
allowCtx, err := newAccessCtx(clients, nil, nil, privateNets)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
ip netip.Addr
|
||||
name string
|
||||
wantRule string
|
||||
wantBlocked bool
|
||||
ip netip.Addr
|
||||
want assert.BoolAssertionFunc
|
||||
name string
|
||||
wantRule string
|
||||
}{{
|
||||
ip: netip.MustParseAddr("1.2.3.4"),
|
||||
name: "match_ip",
|
||||
wantRule: "1.2.3.4",
|
||||
wantBlocked: true,
|
||||
ip: netip.MustParseAddr("1.2.3.4"),
|
||||
name: "match_ip",
|
||||
wantRule: "1.2.3.4",
|
||||
want: assert.False,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("5.6.7.100"),
|
||||
name: "match_cidr",
|
||||
wantRule: "5.6.7.8/24",
|
||||
wantBlocked: true,
|
||||
ip: netip.MustParseAddr("5.6.7.100"),
|
||||
name: "match_cidr",
|
||||
wantRule: "5.6.7.8/24",
|
||||
want: assert.False,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("9.2.3.4"),
|
||||
name: "no_match_ip",
|
||||
wantRule: "",
|
||||
wantBlocked: false,
|
||||
ip: netip.MustParseAddr("9.2.3.4"),
|
||||
name: "no_match_ip",
|
||||
wantRule: "",
|
||||
want: assert.True,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("9.6.7.100"),
|
||||
name: "no_match_cidr",
|
||||
wantRule: "",
|
||||
wantBlocked: false,
|
||||
ip: netip.MustParseAddr("9.6.7.100"),
|
||||
name: "no_match_cidr",
|
||||
wantRule: "",
|
||||
want: assert.True,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("127.0.0.1"),
|
||||
name: "locally_served_ip",
|
||||
wantRule: "",
|
||||
want: assert.False,
|
||||
}}
|
||||
|
||||
t.Run("allow", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
blocked, rule := allowCtx.isBlockedIP(tc.ip)
|
||||
assert.Equal(t, !tc.wantBlocked, blocked)
|
||||
tc.want(t, blocked)
|
||||
assert.Equal(t, tc.wantRule, rule)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("block", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
blocked, rule := blockCtx.isBlockedIP(tc.ip)
|
||||
assert.Equal(t, tc.wantBlocked, blocked)
|
||||
assert.Equal(t, tc.wantRule, rule)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessManager_IsBlockedIP_block(t *testing.T) {
|
||||
clients := []string{
|
||||
"1.2.3.4",
|
||||
"5.6.7.8/24",
|
||||
}
|
||||
|
||||
privateNets := netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
||||
blockCtx, err := newAccessCtx(nil, clients, nil, privateNets)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
ip netip.Addr
|
||||
want assert.BoolAssertionFunc
|
||||
name string
|
||||
wantRule string
|
||||
}{{
|
||||
ip: netip.MustParseAddr("1.2.3.4"),
|
||||
name: "match_ip",
|
||||
wantRule: "1.2.3.4",
|
||||
want: assert.True,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("5.6.7.100"),
|
||||
name: "match_cidr",
|
||||
wantRule: "5.6.7.8/24",
|
||||
want: assert.True,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("9.2.3.4"),
|
||||
name: "no_match_ip",
|
||||
wantRule: "",
|
||||
want: assert.False,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("9.6.7.100"),
|
||||
name: "no_match_cidr",
|
||||
wantRule: "",
|
||||
want: assert.False,
|
||||
}, {
|
||||
ip: netip.MustParseAddr("127.0.0.1"),
|
||||
name: "locally_served_ip",
|
||||
wantRule: "",
|
||||
want: assert.False,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
blocked, rule := blockCtx.isBlockedIP(tc.ip)
|
||||
tc.want(t, blocked)
|
||||
assert.Equal(t, tc.wantRule, rule)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +509,7 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
|
||||
s.conf.AllowedClients,
|
||||
s.conf.DisallowedClients,
|
||||
s.conf.BlockedHosts,
|
||||
s.privateNets,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing access: %w", err)
|
||||
@@ -700,10 +701,12 @@ func (s *Server) IsBlockedClient(ip netip.Addr, clientID string) (blocked bool,
|
||||
blockedByIP := false
|
||||
if ip != (netip.Addr{}) {
|
||||
blockedByIP, rule = s.access.isBlockedIP(ip)
|
||||
log.Debug("by ip %v", blockedByIP)
|
||||
}
|
||||
|
||||
allowlistMode := s.access.allowlistMode()
|
||||
blockedByClientID := s.access.isBlockedClientID(clientID)
|
||||
log.Debug("by client id %v", blockedByClientID)
|
||||
|
||||
// Allow if at least one of the checks allows in allowlist mode, but block
|
||||
// if at least one of the checks blocks in blocklist mode.
|
||||
|
||||
@@ -555,6 +555,7 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
|
||||
|
||||
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
|
||||
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
|
||||
registerHTTP(http.MethodPut, "/control/rewrite/update", d.handleRewriteUpdate)
|
||||
registerHTTP(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
|
||||
|
||||
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// TODO(d.kolyshev): Use [rewrite.Item] instead.
|
||||
@@ -91,3 +92,62 @@ func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
|
||||
// rewriteUpdateJSON is a struct for JSON object with rewrite rule update info.
|
||||
type rewriteUpdateJSON struct {
|
||||
Target rewriteEntryJSON `json:"target"`
|
||||
Update rewriteEntryJSON `json:"update"`
|
||||
}
|
||||
|
||||
// handleRewriteUpdate is the handler for the PUT /control/rewrite/update HTTP
|
||||
// API.
|
||||
func (d *DNSFilter) handleRewriteUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
updateJSON := rewriteUpdateJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&updateJSON)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
rwDel := &LegacyRewrite{
|
||||
Domain: updateJSON.Target.Domain,
|
||||
Answer: updateJSON.Target.Answer,
|
||||
}
|
||||
|
||||
rwAdd := &LegacyRewrite{
|
||||
Domain: updateJSON.Update.Domain,
|
||||
Answer: updateJSON.Update.Answer,
|
||||
}
|
||||
|
||||
err = rwAdd.normalize()
|
||||
if err != nil {
|
||||
// Shouldn't happen currently, since normalize only returns a non-nil
|
||||
// error when a rewrite is nil, but be change-proof.
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "normalizing: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
index := -1
|
||||
defer func() {
|
||||
if index >= 0 {
|
||||
d.Config.ConfigModified()
|
||||
}
|
||||
}()
|
||||
|
||||
d.confLock.Lock()
|
||||
defer d.confLock.Unlock()
|
||||
|
||||
index = slices.IndexFunc(d.Config.Rewrites, rwDel.equal)
|
||||
if index == -1 {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "target rule not found")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
d.Config.Rewrites = slices.Replace(d.Config.Rewrites, index, index+1, rwAdd)
|
||||
|
||||
log.Debug("rewrite: removed element: %s -> %s", rwDel.Domain, rwDel.Answer)
|
||||
log.Debug("rewrite: added element: %s -> %s", rwAdd.Domain, rwAdd.Answer)
|
||||
}
|
||||
|
||||
237
internal/filtering/rewritehttp_test.go
Normal file
237
internal/filtering/rewritehttp_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package filtering_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO(d.kolyshev): Use [rewrite.Item] instead.
|
||||
type rewriteJSON struct {
|
||||
Domain string `json:"domain"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
type rewriteUpdateJSON struct {
|
||||
Target rewriteJSON `json:"target"`
|
||||
Update rewriteJSON `json:"update"`
|
||||
}
|
||||
|
||||
const (
|
||||
// testTimeout is the common timeout for tests.
|
||||
testTimeout = 100 * time.Millisecond
|
||||
|
||||
listURL = "/control/rewrite/list"
|
||||
addURL = "/control/rewrite/add"
|
||||
deleteURL = "/control/rewrite/delete"
|
||||
updateURL = "/control/rewrite/update"
|
||||
|
||||
decodeErrorMsg = "json.Decode: json: cannot unmarshal string into Go value of type" +
|
||||
" filtering.rewriteEntryJSON\n"
|
||||
)
|
||||
|
||||
func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
|
||||
confModCh := make(chan struct{})
|
||||
reqCh := make(chan struct{})
|
||||
testRewrites := []*rewriteJSON{
|
||||
{Domain: "example.local", Answer: "example.rewrite"},
|
||||
{Domain: "one.local", Answer: "one.rewrite"},
|
||||
}
|
||||
|
||||
testRewritesJSON, mErr := json.Marshal(testRewrites)
|
||||
require.NoError(t, mErr)
|
||||
|
||||
testCases := []struct {
|
||||
reqData any
|
||||
name string
|
||||
url string
|
||||
method string
|
||||
wantList []*rewriteJSON
|
||||
wantBody string
|
||||
wantConfMod bool
|
||||
wantStatus int
|
||||
}{{
|
||||
name: "list",
|
||||
url: listURL,
|
||||
method: http.MethodGet,
|
||||
reqData: nil,
|
||||
wantConfMod: false,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: string(testRewritesJSON) + "\n",
|
||||
wantList: testRewrites,
|
||||
}, {
|
||||
name: "add",
|
||||
url: addURL,
|
||||
method: http.MethodPost,
|
||||
reqData: rewriteJSON{Domain: "add.local", Answer: "add.rewrite"},
|
||||
wantConfMod: true,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: "",
|
||||
wantList: append(
|
||||
testRewrites,
|
||||
&rewriteJSON{Domain: "add.local", Answer: "add.rewrite"},
|
||||
),
|
||||
}, {
|
||||
name: "add_error",
|
||||
url: addURL,
|
||||
method: http.MethodPost,
|
||||
reqData: "invalid_json",
|
||||
wantConfMod: false,
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantBody: decodeErrorMsg,
|
||||
wantList: testRewrites,
|
||||
}, {
|
||||
name: "delete",
|
||||
url: deleteURL,
|
||||
method: http.MethodPost,
|
||||
reqData: rewriteJSON{Domain: "one.local", Answer: "one.rewrite"},
|
||||
wantConfMod: true,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: "",
|
||||
wantList: []*rewriteJSON{{Domain: "example.local", Answer: "example.rewrite"}},
|
||||
}, {
|
||||
name: "delete_error",
|
||||
url: deleteURL,
|
||||
method: http.MethodPost,
|
||||
reqData: "invalid_json",
|
||||
wantConfMod: false,
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantBody: decodeErrorMsg,
|
||||
wantList: testRewrites,
|
||||
}, {
|
||||
name: "update",
|
||||
url: updateURL,
|
||||
method: http.MethodPut,
|
||||
reqData: rewriteUpdateJSON{
|
||||
Target: rewriteJSON{Domain: "one.local", Answer: "one.rewrite"},
|
||||
Update: rewriteJSON{Domain: "upd.local", Answer: "upd.rewrite"},
|
||||
},
|
||||
wantConfMod: true,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: "",
|
||||
wantList: []*rewriteJSON{
|
||||
{Domain: "example.local", Answer: "example.rewrite"},
|
||||
{Domain: "upd.local", Answer: "upd.rewrite"},
|
||||
},
|
||||
}, {
|
||||
name: "update_error",
|
||||
url: updateURL,
|
||||
method: http.MethodPut,
|
||||
reqData: "invalid_json",
|
||||
wantConfMod: false,
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantBody: "json.Decode: json: cannot unmarshal string into Go value of type" +
|
||||
" filtering.rewriteUpdateJSON\n",
|
||||
wantList: testRewrites,
|
||||
}, {
|
||||
name: "update_error_target",
|
||||
url: updateURL,
|
||||
method: http.MethodPut,
|
||||
reqData: rewriteUpdateJSON{
|
||||
Target: rewriteJSON{Domain: "inv.local", Answer: "inv.rewrite"},
|
||||
Update: rewriteJSON{Domain: "upd.local", Answer: "upd.rewrite"},
|
||||
},
|
||||
wantConfMod: false,
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantBody: "target rule not found\n",
|
||||
wantList: testRewrites,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
onConfModified := func() {
|
||||
if !tc.wantConfMod {
|
||||
panic("config modified has been fired")
|
||||
}
|
||||
|
||||
testutil.RequireSend(testutil.PanicT{}, confModCh, struct{}{}, testTimeout)
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
handlers := make(map[string]http.Handler)
|
||||
|
||||
d, err := filtering.New(&filtering.Config{
|
||||
ConfigModified: onConfModified,
|
||||
HTTPRegister: func(_, url string, handler http.HandlerFunc) {
|
||||
handlers[url] = handler
|
||||
},
|
||||
Rewrites: rewriteEntriesToLegacyRewrites(testRewrites),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(d.Close)
|
||||
|
||||
d.RegisterFilteringHandlers()
|
||||
require.NotEmpty(t, handlers)
|
||||
require.Contains(t, handlers, listURL)
|
||||
require.Contains(t, handlers, tc.url)
|
||||
|
||||
var body io.Reader
|
||||
if tc.reqData != nil {
|
||||
data, rErr := json.Marshal(tc.reqData)
|
||||
require.NoError(t, rErr)
|
||||
|
||||
body = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(tc.method, tc.url, body)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
go func() {
|
||||
handlers[tc.url].ServeHTTP(w, r)
|
||||
|
||||
testutil.RequireSend(testutil.PanicT{}, reqCh, struct{}{}, testTimeout)
|
||||
}()
|
||||
|
||||
if tc.wantConfMod {
|
||||
testutil.RequireReceive(t, confModCh, testTimeout)
|
||||
}
|
||||
|
||||
testutil.RequireReceive(t, reqCh, testTimeout)
|
||||
assert.Equal(t, tc.wantStatus, w.Code)
|
||||
|
||||
respBody, err := io.ReadAll(w.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte(tc.wantBody), respBody)
|
||||
|
||||
assertRewritesList(t, handlers[listURL], tc.wantList)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// assertRewritesList checks if rewrites list equals the list received from the
|
||||
// handler by listURL.
|
||||
func assertRewritesList(t *testing.T, handler http.Handler, wantList []*rewriteJSON) {
|
||||
t.Helper()
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, listURL, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var actual []*rewriteJSON
|
||||
err := json.NewDecoder(w.Body).Decode(&actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, wantList, actual)
|
||||
}
|
||||
|
||||
// rewriteEntriesToLegacyRewrites gets legacy rewrites from json entries.
|
||||
func rewriteEntriesToLegacyRewrites(entries []*rewriteJSON) (rw []*filtering.LegacyRewrite) {
|
||||
for _, entry := range entries {
|
||||
rw = append(rw, &filtering.LegacyRewrite{
|
||||
Domain: entry.Domain,
|
||||
Answer: entry.Answer,
|
||||
})
|
||||
}
|
||||
|
||||
return rw
|
||||
}
|
||||
@@ -12,6 +12,18 @@
|
||||
`GET /control/dhcp/interfaces` HTTP APIs is now correctly set to
|
||||
`application/json` as opposed to `text/plain`.
|
||||
|
||||
### New HTTP API 'PUT /control/rewrite/update'
|
||||
|
||||
* The new `PUT /control/rewrite/update` HTTP API allows rewrite rule updates.
|
||||
It accepts a JSON object with the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"target": {"domain":"example.com","answer":"answer-to-update"},
|
||||
"update": {"domain":"example.com","answer":"new-answer"}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## v0.107.29: API changes
|
||||
|
||||
@@ -1061,6 +1061,17 @@
|
||||
'responses':
|
||||
'200':
|
||||
'description': 'OK.'
|
||||
'/rewrite/update':
|
||||
'put':
|
||||
'tags':
|
||||
- 'rewrite'
|
||||
'operationId': 'rewriteUpdate'
|
||||
'summary': 'Update a Rewrite rule'
|
||||
'requestBody':
|
||||
'$ref': '#/components/requestBodies/RewriteUpdate'
|
||||
'responses':
|
||||
'200':
|
||||
'description': 'OK.'
|
||||
'/i18n/change_language':
|
||||
'post':
|
||||
'deprecated': true
|
||||
@@ -1311,6 +1322,12 @@
|
||||
'schema':
|
||||
'$ref': '#/components/schemas/RewriteEntry'
|
||||
'required': true
|
||||
'RewriteUpdate':
|
||||
'content':
|
||||
'application/json':
|
||||
'schema':
|
||||
'$ref': '#/components/schemas/RewriteUpdate'
|
||||
'required': true
|
||||
'schemas':
|
||||
'ServerStatus':
|
||||
'type': 'object'
|
||||
@@ -2702,6 +2719,14 @@
|
||||
'items':
|
||||
'$ref': '#/components/schemas/RewriteEntry'
|
||||
'description': 'Rewrite rules array'
|
||||
'RewriteUpdate':
|
||||
'type': 'object'
|
||||
'description': 'Rewrite rule update object'
|
||||
'properties':
|
||||
'target':
|
||||
'$ref': '#/components/schemas/RewriteEntry'
|
||||
'update':
|
||||
'$ref': '#/components/schemas/RewriteEntry'
|
||||
'RewriteEntry':
|
||||
'type': 'object'
|
||||
'description': 'Rewrite rule'
|
||||
|
||||
Reference in New Issue
Block a user