[feature] Implement explicit domain allows + allowlist federation mode (#2200)

* love like winter! wohoah, wohoah

* domain allow side effects

* tests! logging! unallow!

* document federation modes

* linty linterson

* test

* further adventures in documentation

* finish up domain block documentation (i think)

* change wording a wee little bit

* docs, example

* consolidate shared domainPermission code

* call mode once

* fetch federation mode within domain blocked func

* read domain perm import in streaming manner

* don't use pointer to slice for domain perms

* don't bother copying blocks + allows before deleting

* admonish!

* change wording just a scooch

* update docs
This commit is contained in:
tobi 2023-09-21 12:12:04 +02:00 committed by GitHub
commit 183eaa5b29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2877 additions and 730 deletions

View file

@ -26,23 +26,28 @@ import (
"golang.org/x/exp/slices"
)
// BlockCache provides a means of caching domain blocks in memory to reduce load
// on an underlying storage mechanism, e.g. a database.
// Cache provides a means of caching domains in memory to reduce
// load on an underlying storage mechanism, e.g. a database.
//
// The in-memory block list is kept up-to-date by means of a passed loader function during every
// call to .IsBlocked(). In the case of a nil internal block list, the loader function is called to
// hydrate the cache with the latest list of domain blocks. The .Clear() function can be used to
// invalidate the cache, e.g. when a domain block is added / deleted from the database.
type BlockCache struct {
// The in-memory domain list is kept up-to-date by means of a passed
// loader function during every call to .Matches(). In the case of
// a nil internal domain list, the loader function is called to hydrate
// the cache with the latest list of domains.
//
// The .Clear() function can be used to invalidate the cache,
// e.g. when an entry is added / deleted from the database.
type Cache struct {
// atomically updated ptr value to the
// current domain block cache radix trie.
// current domain cache radix trie.
rootptr unsafe.Pointer
}
// IsBlocked checks whether domain is blocked. If the cache is not currently loaded, then the provided load function is used to hydrate it.
func (b *BlockCache) IsBlocked(domain string, load func() ([]string, error)) (bool, error) {
// Matches checks whether domain matches an entry in the cache.
// If the cache is not currently loaded, then the provided load
// function is used to hydrate it.
func (c *Cache) Matches(domain string, load func() ([]string, error)) (bool, error) {
// Load the current root pointer value.
ptr := atomic.LoadPointer(&b.rootptr)
ptr := atomic.LoadPointer(&c.rootptr)
if ptr == nil {
// Cache is not hydrated.
@ -67,7 +72,7 @@ func (b *BlockCache) IsBlocked(domain string, load func() ([]string, error)) (bo
// Store the new node ptr.
ptr = unsafe.Pointer(root)
atomic.StorePointer(&b.rootptr, ptr)
atomic.StorePointer(&c.rootptr, ptr)
}
// Look for a match in the trie node.
@ -75,22 +80,20 @@ func (b *BlockCache) IsBlocked(domain string, load func() ([]string, error)) (bo
}
// Clear will drop the currently loaded domain list,
// triggering a reload on next call to .IsBlocked().
func (b *BlockCache) Clear() {
atomic.StorePointer(&b.rootptr, nil)
// triggering a reload on next call to .Matches().
func (c *Cache) Clear() {
atomic.StorePointer(&c.rootptr, nil)
}
// String returns a string representation of stored domains in block cache.
func (b *BlockCache) String() string {
if ptr := atomic.LoadPointer(&b.rootptr); ptr != nil {
// String returns a string representation of stored domains in cache.
func (c *Cache) String() string {
if ptr := atomic.LoadPointer(&c.rootptr); ptr != nil {
return (*root)(ptr).String()
}
return "<empty>"
}
// root is the root node in the domain
// block cache radix trie. this is the
// singular access point to the trie.
// root is the root node in the domain cache radix trie. this is the singular access point to the trie.
type root struct{ root node }
// Add will add the given domain to the radix trie.
@ -99,14 +102,14 @@ func (r *root) Add(domain string) {
}
// Match will return whether the given domain matches
// an existing stored domain block in this radix trie.
// an existing stored domain in this radix trie.
func (r *root) Match(domain string) bool {
return r.root.match(strings.Split(domain, "."))
}
// Sort will sort the entire radix trie ensuring that
// child nodes are stored in alphabetical order. This
// MUST be done to finalize the block cache in order
// MUST be done to finalize the domain cache in order
// to speed up the binary search of node child parts.
func (r *root) Sort() {
r.root.sort()
@ -154,7 +157,7 @@ func (n *node) add(parts []string) {
if len(parts) == 0 {
// Drop all children here as
// this is a higher-level block
// this is a higher-level domain
// than that we previously had.
nn.child = nil
return

View file

@ -24,21 +24,21 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/cache/domain"
)
func TestBlockCache(t *testing.T) {
c := new(domain.BlockCache)
func TestCache(t *testing.T) {
c := new(domain.Cache)
blocks := []string{
cachedDomains := []string{
"google.com",
"google.co.uk",
"pleroma.bad.host",
}
loader := func() ([]string, error) {
t.Log("load: returning blocked domains")
return blocks, nil
t.Log("load: returning cached domains")
return cachedDomains, nil
}
// Check a list of known blocked domains.
// Check a list of known cached domains.
for _, domain := range []string{
"google.com",
"mail.google.com",
@ -47,13 +47,13 @@ func TestBlockCache(t *testing.T) {
"pleroma.bad.host",
"dev.pleroma.bad.host",
} {
t.Logf("checking domain is blocked: %s", domain)
if b, _ := c.IsBlocked(domain, loader); !b {
t.Errorf("domain should be blocked: %s", domain)
t.Logf("checking domain matches: %s", domain)
if b, _ := c.Matches(domain, loader); !b {
t.Errorf("domain should be matched: %s", domain)
}
}
// Check a list of known unblocked domains.
// Check a list of known uncached domains.
for _, domain := range []string{
"askjeeves.com",
"ask-kim.co.uk",
@ -62,9 +62,9 @@ func TestBlockCache(t *testing.T) {
"gts.bad.host",
"mastodon.bad.host",
} {
t.Logf("checking domain isn't blocked: %s", domain)
if b, _ := c.IsBlocked(domain, loader); b {
t.Errorf("domain should not be blocked: %s", domain)
t.Logf("checking domain isn't matched: %s", domain)
if b, _ := c.Matches(domain, loader); b {
t.Errorf("domain should not be matched: %s", domain)
}
}
@ -76,10 +76,10 @@ func TestBlockCache(t *testing.T) {
knownErr := errors.New("known error")
// Check that reload is actually performed and returns our error
if _, err := c.IsBlocked("", func() ([]string, error) {
if _, err := c.Matches("", func() ([]string, error) {
t.Log("load: returning known error")
return nil, knownErr
}); !errors.Is(err, knownErr) {
t.Errorf("is blocked did not return expected error: %v", err)
t.Errorf("matches did not return expected error: %v", err)
}
}