feat: Add blacklist support for public relays
This commit is contained in:
@@ -43,6 +43,7 @@ type C struct {
|
||||
Owners []string `env:"ORLY_OWNERS" usage:"list of users whose follow lists designate whitelisted users who can publish events, and who can read if public readable is false (comma separated)"`
|
||||
Private bool `env:"ORLY_PRIVATE" usage:"do not spider for user metadata because the relay is private and this would leak relay memberships" default:"false"`
|
||||
Whitelist []string `env:"ORLY_WHITELIST" usage:"only allow connections from this list of IP addresses"`
|
||||
Blacklist []string `env:"ORLY_BLACKLIST" usage:"list of pubkeys to block when auth is not required (comma separated)"`
|
||||
RelaySecret string `env:"ORLY_SECRET_KEY" usage:"secret key for relay cluster replication authentication"`
|
||||
PeerRelays []string `env:"ORLY_PEER_RELAYS" usage:"list of peer relays URLs that new events are pushed to in format <pubkey>|<url>"`
|
||||
}
|
||||
|
||||
@@ -42,6 +42,15 @@ func (s *Server) AcceptEvent(
|
||||
remote string,
|
||||
) (accept bool, notice string, afterSave func()) {
|
||||
if !s.AuthRequired() {
|
||||
// Check blacklist for public relay mode
|
||||
if len(s.blacklistPubkeys) > 0 {
|
||||
for _, blockedPubkey := range s.blacklistPubkeys {
|
||||
if bytes.Equal(blockedPubkey, ev.Pubkey) {
|
||||
notice = "event author is blacklisted"
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
accept = true
|
||||
return
|
||||
}
|
||||
@@ -64,6 +73,7 @@ func (s *Server) AcceptEvent(
|
||||
for _, u := range s.OwnersMuted() {
|
||||
if bytes.Equal(u, authedPubkey) {
|
||||
notice = "event author is banned from this relay"
|
||||
accept = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
// mockServerForEvent is a simple mock implementation of the Server struct for testing AcceptEvent
|
||||
type mockServerForEvent struct {
|
||||
authRequired bool
|
||||
ownersFollowed [][]byte
|
||||
authRequired bool
|
||||
ownersFollowed [][]byte
|
||||
followedFollows [][]byte
|
||||
}
|
||||
|
||||
@@ -203,8 +203,8 @@ func TestAcceptEventWithRealServer(t *testing.T) {
|
||||
if accept {
|
||||
t.Error("AcceptEvent() accept = true, want false")
|
||||
}
|
||||
if notice != "" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
|
||||
if notice != "client isn't authed" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want 'client isn't authed'", notice)
|
||||
}
|
||||
if afterSave != nil {
|
||||
t.Error("AcceptEvent() afterSave is not nil, but should be nil")
|
||||
@@ -234,4 +234,81 @@ func TestAcceptEventWithRealServer(t *testing.T) {
|
||||
if !accept {
|
||||
t.Error("AcceptEvent() accept = false, want true")
|
||||
}
|
||||
|
||||
// Test with muted user
|
||||
s.SetOwnersMuted([][]byte{[]byte("test-pubkey")})
|
||||
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
|
||||
if accept {
|
||||
t.Error("AcceptEvent() accept = true, want false")
|
||||
}
|
||||
if notice != "event author is banned from this relay" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want 'event author is banned from this relay'", notice)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAcceptEventWithBlacklist tests the blacklist functionality when auth is not required
|
||||
func TestAcceptEventWithBlacklist(t *testing.T) {
|
||||
// Create a context and HTTP request for testing
|
||||
ctx := context.Bg()
|
||||
req, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||
|
||||
// Test pubkey bytes
|
||||
testPubkey := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20}
|
||||
blockedPubkey := []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30}
|
||||
|
||||
// Test with public relay mode (auth not required) and no blacklist
|
||||
s := &Server{
|
||||
C: &config.C{
|
||||
AuthRequired: false,
|
||||
},
|
||||
Lists: new(Lists),
|
||||
}
|
||||
|
||||
// Create event with test pubkey
|
||||
testEvent := &event.E{}
|
||||
testEvent.Pubkey = testPubkey
|
||||
|
||||
// Should accept when no blacklist
|
||||
accept, notice, _ := s.AcceptEvent(ctx, testEvent, req, nil, "127.0.0.1")
|
||||
if !accept {
|
||||
t.Error("AcceptEvent() accept = false, want true")
|
||||
}
|
||||
if notice != "" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
|
||||
}
|
||||
|
||||
// Add blacklist with different pubkey
|
||||
s.blacklistPubkeys = [][]byte{blockedPubkey}
|
||||
|
||||
// Should still accept when author not in blacklist
|
||||
accept, notice, _ = s.AcceptEvent(ctx, testEvent, req, nil, "127.0.0.1")
|
||||
if !accept {
|
||||
t.Error("AcceptEvent() accept = false, want true")
|
||||
}
|
||||
if notice != "" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
|
||||
}
|
||||
|
||||
// Create event with blocked pubkey
|
||||
blockedEvent := &event.E{}
|
||||
blockedEvent.Pubkey = blockedPubkey
|
||||
|
||||
// Should reject when author is in blacklist
|
||||
accept, notice, _ = s.AcceptEvent(ctx, blockedEvent, req, nil, "127.0.0.1")
|
||||
if accept {
|
||||
t.Error("AcceptEvent() accept = true, want false")
|
||||
}
|
||||
if notice != "event author is blacklisted" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want 'event author is blacklisted'", notice)
|
||||
}
|
||||
|
||||
// Test with auth required - blacklist should not apply
|
||||
s.C.AuthRequired = true
|
||||
accept, notice, _ = s.AcceptEvent(ctx, blockedEvent, req, nil, "127.0.0.1")
|
||||
if accept {
|
||||
t.Error("AcceptEvent() accept = true, want false")
|
||||
}
|
||||
if notice != "client isn't authed" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want 'client isn't authed'", notice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,41 +8,41 @@ import (
|
||||
func TestLists_OwnersPubkeys(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
|
||||
// Test with empty list
|
||||
pks := l.OwnersPubkeys()
|
||||
if len(pks) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(pks))
|
||||
}
|
||||
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("pubkey1"),
|
||||
[]byte("pubkey2"),
|
||||
[]byte("pubkey3"),
|
||||
}
|
||||
|
||||
|
||||
l.SetOwnersPubkeys(testPubkeys)
|
||||
|
||||
|
||||
// Verify length
|
||||
if l.LenOwnersPubkeys() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersPubkeys())
|
||||
}
|
||||
|
||||
|
||||
// Verify content
|
||||
pks = l.OwnersPubkeys()
|
||||
if len(pks) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d pubkeys, got %d", len(testPubkeys), len(pks))
|
||||
}
|
||||
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range pks {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Pubkey at index %d doesn't match: expected %s, got %s",
|
||||
t.Errorf("Pubkey at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Verify that the returned slice is a copy, not a reference
|
||||
pks[0] = []byte("modified")
|
||||
newPks := l.OwnersPubkeys()
|
||||
@@ -54,37 +54,37 @@ func TestLists_OwnersPubkeys(t *testing.T) {
|
||||
func TestLists_OwnersFollowed(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
|
||||
// Test with empty list
|
||||
followed := l.OwnersFollowed()
|
||||
if len(followed) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(followed))
|
||||
}
|
||||
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("followed1"),
|
||||
[]byte("followed2"),
|
||||
[]byte("followed3"),
|
||||
}
|
||||
|
||||
|
||||
l.SetOwnersFollowed(testPubkeys)
|
||||
|
||||
|
||||
// Verify length
|
||||
if l.LenOwnersFollowed() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersFollowed())
|
||||
}
|
||||
|
||||
|
||||
// Verify content
|
||||
followed = l.OwnersFollowed()
|
||||
if len(followed) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d followed, got %d", len(testPubkeys), len(followed))
|
||||
}
|
||||
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range followed {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Followed at index %d doesn't match: expected %s, got %s",
|
||||
t.Errorf("Followed at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
@@ -93,37 +93,37 @@ func TestLists_OwnersFollowed(t *testing.T) {
|
||||
func TestLists_FollowedFollows(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
|
||||
// Test with empty list
|
||||
follows := l.FollowedFollows()
|
||||
if len(follows) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(follows))
|
||||
}
|
||||
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("follow1"),
|
||||
[]byte("follow2"),
|
||||
[]byte("follow3"),
|
||||
}
|
||||
|
||||
|
||||
l.SetFollowedFollows(testPubkeys)
|
||||
|
||||
|
||||
// Verify length
|
||||
if l.LenFollowedFollows() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenFollowedFollows())
|
||||
}
|
||||
|
||||
|
||||
// Verify content
|
||||
follows = l.FollowedFollows()
|
||||
if len(follows) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d follows, got %d", len(testPubkeys), len(follows))
|
||||
}
|
||||
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range follows {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Follow at index %d doesn't match: expected %s, got %s",
|
||||
t.Errorf("Follow at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
@@ -132,37 +132,37 @@ func TestLists_FollowedFollows(t *testing.T) {
|
||||
func TestLists_OwnersMuted(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
|
||||
// Test with empty list
|
||||
muted := l.OwnersMuted()
|
||||
if len(muted) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(muted))
|
||||
}
|
||||
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("muted1"),
|
||||
[]byte("muted2"),
|
||||
[]byte("muted3"),
|
||||
}
|
||||
|
||||
|
||||
l.SetOwnersMuted(testPubkeys)
|
||||
|
||||
|
||||
// Verify length
|
||||
if l.LenOwnersMuted() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersMuted())
|
||||
}
|
||||
|
||||
|
||||
// Verify content
|
||||
muted = l.OwnersMuted()
|
||||
if len(muted) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d muted, got %d", len(testPubkeys), len(muted))
|
||||
}
|
||||
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range muted {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Muted at index %d doesn't match: expected %s, got %s",
|
||||
t.Errorf("Muted at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
@@ -171,10 +171,10 @@ func TestLists_OwnersMuted(t *testing.T) {
|
||||
func TestLists_ConcurrentAccess(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
|
||||
// Test concurrent access to the lists
|
||||
done := make(chan bool)
|
||||
|
||||
|
||||
// Concurrent reads and writes
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
@@ -183,7 +183,7 @@ func TestLists_ConcurrentAccess(t *testing.T) {
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
l.SetOwnersFollowed([][]byte{[]byte("followed1"), []byte("followed2")})
|
||||
@@ -191,7 +191,7 @@ func TestLists_ConcurrentAccess(t *testing.T) {
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
l.SetFollowedFollows([][]byte{[]byte("follow1"), []byte("follow2")})
|
||||
@@ -199,7 +199,7 @@ func TestLists_ConcurrentAccess(t *testing.T) {
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
l.SetOwnersMuted([][]byte{[]byte("muted1"), []byte("muted2")})
|
||||
@@ -207,11 +207,11 @@ func TestLists_ConcurrentAccess(t *testing.T) {
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 4; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
|
||||
// If we got here without deadlocks or panics, the test passes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"orly.dev/pkg/protocol/servemux"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/keys"
|
||||
"orly.dev/pkg/utils/log"
|
||||
|
||||
"github.com/rs/cors"
|
||||
@@ -29,14 +30,15 @@ import (
|
||||
// encapsulates various components such as context, cancel function, options,
|
||||
// relay interface, address, HTTP server, and configuration settings.
|
||||
type Server struct {
|
||||
Ctx context.T
|
||||
Cancel context.F
|
||||
options *options.T
|
||||
relay relay.I
|
||||
Addr string
|
||||
mux *servemux.S
|
||||
httpServer *http.Server
|
||||
listeners *publish.S
|
||||
Ctx context.T
|
||||
Cancel context.F
|
||||
options *options.T
|
||||
relay relay.I
|
||||
Addr string
|
||||
mux *servemux.S
|
||||
httpServer *http.Server
|
||||
listeners *publish.S
|
||||
blacklistPubkeys [][]byte
|
||||
*config.C
|
||||
*Lists
|
||||
*Peers
|
||||
@@ -105,6 +107,14 @@ func NewServer(
|
||||
Lists: new(Lists),
|
||||
Peers: new(Peers),
|
||||
}
|
||||
// Parse blacklist pubkeys
|
||||
for _, v := range s.C.Blacklist {
|
||||
var pk []byte
|
||||
if pk, err = keys.DecodeNpubOrHex(v); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
s.blacklistPubkeys = append(s.blacklistPubkeys, pk)
|
||||
}
|
||||
chk.E(
|
||||
s.Peers.Init(sp.C.PeerRelays, sp.C.RelaySecret),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user