diff --git a/cmd/replicatrd/policies/nip04.go b/cmd/replicatrd/policies/nip04.go deleted file mode 100644 index 471b7d71..00000000 --- a/cmd/replicatrd/policies/nip04.go +++ /dev/null @@ -1,38 +0,0 @@ -package policies - -import ( - "context" - - "github.com/Hubmakerlabs/replicatr/cmd/replicatrd/replicatr" - "github.com/nbd-wtf/go-nostr" - "golang.org/x/exp/slices" -) - -// RejectKind04Snoopers prevents reading NIP-04 messages from people not involved in the conversation. -func RejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, string) { - // prevent kind-4 events from being returned to unauthed users, - // only when authentication is a thing - if !slices.Contains(filter.Kinds, 4) { - return false, "" - } - - ws := replicatr.GetConnection(ctx) - senders := filter.Authors - receivers, _ := filter.Tags["p"] - switch { - case ws.AuthedPublicKey == "": - // not authenticated - return true, "restricted: this relay does not serve kind-4 to unauthenticated users, does your client implement NIP-42?" - case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.AuthedPublicKey): - // allowed filter: ws.authed is sole sender (filter specifies one or all receivers) - return false, "" - case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.AuthedPublicKey): - // allowed filter: ws.authed is sole receiver (filter specifies one or all senders) - return false, "" - default: - // restricted filter: do not return any events, - // even if other elements in filters array were not restricted). - // client should know better. - return true, "restricted: authenticated user does not have authorization for requested filters." - } -} diff --git a/cmd/replicatrd/replicatr/alias.go b/cmd/replicatrd/replicatr/alias.go new file mode 100644 index 00000000..8783ca81 --- /dev/null +++ b/cmd/replicatrd/replicatr/alias.go @@ -0,0 +1,39 @@ +package replicatr + +import ( + "context" + "net/http" + "sync" + + "github.com/fasthttp/websocket" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip11" + "github.com/puzpuzpuz/xsync/v2" +) + +// aliases so we can swap out to another package with only here changed +type ( + Ctx = context.Context + Info = nip11.RelayInformationDocument + Event = nostr.Event + Filter = nostr.Filter + Filters = nostr.Filters + TagMap = nostr.TagMap + EventEnvelope = nostr.EventEnvelope + OKEnvelope = nostr.OKEnvelope + CountEnvelope = nostr.CountEnvelope + ClosedEnvelope = nostr.ClosedEnvelope + ReqEnvelope = nostr.ReqEnvelope + EOSEEnvelope = nostr.EOSEEnvelope + CloseEnvelope = nostr.CloseEnvelope + AuthEnvelope = nostr.AuthEnvelope + NoticeEnvelope = nostr.NoticeEnvelope + Conn = websocket.Conn + Request = http.Request + ResponseWriter = http.ResponseWriter + Mutex = sync.Mutex + WaitGroup = sync.WaitGroup + CancelCauseFunc = context.CancelCauseFunc + ListenerMap = *xsync.MapOf[string, *Listener] + Timestamp = nostr.Timestamp +) diff --git a/cmd/replicatrd/policies/events.go b/cmd/replicatrd/replicatr/events.go similarity index 55% rename from cmd/replicatrd/policies/events.go rename to cmd/replicatrd/replicatr/events.go index 3bcb32e6..eec7cd67 100644 --- a/cmd/replicatrd/policies/events.go +++ b/cmd/replicatrd/replicatr/events.go @@ -1,18 +1,20 @@ -package policies +package replicatr import ( - "context" - "github.com/nbd-wtf/go-nostr" "golang.org/x/exp/slices" ) -// PreventTooManyIndexableTags returns a function that can be used as a RejectFilter that will reject -// events with more indexable (single-character) tags than the specified number. +// PreventTooManyIndexableTags returns a function that can be used as a +// RejectFilter that will reject events with more indexable (single-character) +// tags than the specified number. // -// If ignoreKinds is given this restriction will not apply to these kinds (useful for allowing a bigger). -// If onlyKinds is given then all other kinds will be ignored. -func PreventTooManyIndexableTags(max int, ignoreKinds []int, onlyKinds []int) func(context.Context, *nostr.Event) (bool, string) { +// If ignoreKinds is given this restriction will not apply to these kinds +// (useful for allowing a bigger). If onlyKinds is given then all other kinds +// will be ignored. +func PreventTooManyIndexableTags(max int, ignoreKinds []int, + onlyKinds []int) func(Ctx, *Event) (bool, string) { + ignore := func(kind int) bool { return false } if len(ignoreKinds) > 0 { ignore = func(kind int) bool { @@ -27,7 +29,7 @@ func PreventTooManyIndexableTags(max int, ignoreKinds []int, onlyKinds []int) fu } } - return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) { + return func(ctx Ctx, event *Event) (reject bool, msg string) { if ignore(event.Kind) { return false, "" } @@ -45,9 +47,10 @@ func PreventTooManyIndexableTags(max int, ignoreKinds []int, onlyKinds []int) fu } } -// PreventLargeTags rejects events that have indexable tag values greater than maxTagValueLen. -func PreventLargeTags(maxTagValueLen int) func(context.Context, *nostr.Event) (bool, string) { - return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) { +// PreventLargeTags rejects events that have indexable tag values greater than +// maxTagValueLen. +func PreventLargeTags(maxTagValueLen int) func(Ctx, *Event) (bool, string) { + return func(ctx Ctx, event *Event) (reject bool, msg string) { for _, tag := range event.Tags { if len(tag) > 1 && len(tag[0]) == 1 { if len(tag[1]) > maxTagValueLen { @@ -59,30 +62,29 @@ func PreventLargeTags(maxTagValueLen int) func(context.Context, *nostr.Event) (b } } -// RestrictToSpecifiedKinds returns a function that can be used as a RejectFilter that will reject -// any events with kinds different than the specified ones. -func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Event) (bool, string) { - max := 0 - min := 0 +// RestrictToSpecifiedKinds returns a function that can be used as a +// RejectFilter that will reject any events with kinds different than the +// specified ones. +func RestrictToSpecifiedKinds(kinds ...uint16) func(Ctx, *Event) (bool, string) { + kMax := 0 + kMin := 0 for _, kind := range kinds { - if int(kind) > max { - max = int(kind) + if int(kind) > kMax { + kMax = int(kind) } - if int(kind) < min { - min = int(kind) + if int(kind) < kMin { + kMin = int(kind) } } - - return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) { + return func(ctx Ctx, event *Event) (reject bool, msg string) { // these are cheap and very questionable optimizations, but they exist for a reason: // we would have to ensure that the kind number is within the bounds of a uint16 anyway - if event.Kind > max { + if event.Kind > kMax { return true, "event kind not allowed" } - if event.Kind < min { + if event.Kind < kMin { return true, "event kind not allowed" } - // hopefully this map of uint16s is very fast if _, allowed := slices.BinarySearch(kinds, uint16(event.Kind)); allowed { return false, "" @@ -91,8 +93,8 @@ func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Even } } -func PreventTimestampsInThePast(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, string) { - return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) { +func PreventTimestampsInThePast(thresholdSeconds Timestamp) func(Ctx, *Event) (bool, string) { + return func(ctx Ctx, event *Event) (reject bool, msg string) { if nostr.Now()-event.CreatedAt > thresholdSeconds { return true, "event too old" } @@ -100,8 +102,8 @@ func PreventTimestampsInThePast(thresholdSeconds nostr.Timestamp) func(context.C } } -func PreventTimestampsInTheFuture(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, string) { - return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) { +func PreventTimestampsInTheFuture(thresholdSeconds Timestamp) func(Ctx, *Event) (bool, string) { + return func(ctx Ctx, event *Event) (reject bool, msg string) { if event.CreatedAt-nostr.Now() > thresholdSeconds { return true, "event too much in the future" } diff --git a/cmd/replicatrd/policies/filters.go b/cmd/replicatrd/replicatr/filters.go similarity index 60% rename from cmd/replicatrd/policies/filters.go rename to cmd/replicatrd/replicatr/filters.go index 72ed356f..58c23274 100644 --- a/cmd/replicatrd/policies/filters.go +++ b/cmd/replicatrd/replicatr/filters.go @@ -1,4 +1,4 @@ -package policies +package replicatr import ( "context" @@ -8,18 +8,17 @@ import ( ) // NoComplexFilters disallows filters with more than 2 tags. -func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { +func NoComplexFilters(ctx Ctx, filter *Filter) (reject bool, msg string) { items := len(filter.Tags) + len(filter.Kinds) - if items > 4 && len(filter.Tags) > 2 { return true, "too many things to filter for" } - return false, "" } -// NoEmptyFilters disallows filters that don't have at least a tag, a kind, an author or an id. -func NoEmptyFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { +// NoEmptyFilters disallows filters that don't have at least a tag, a kind, an +// author or an id. +func NoEmptyFilters(ctx Ctx, filter *Filter) (reject bool, msg string) { c := len(filter.Kinds) + len(filter.IDs) + len(filter.Authors) for _, tagItems := range filter.Tags { c += len(tagItems) @@ -30,26 +29,26 @@ func NoEmptyFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg return false, "" } -// AntiSyncBots tries to prevent people from syncing kind:1s from this relay to else by always -// requiring an author parameter at least. -func AntiSyncBots(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { +// AntiSyncBots tries to prevent people from syncing kind:1s from this relay to +// else by always requiring an author parameter at least. +func AntiSyncBots(ctx Ctx, filter *Filter) (reject bool, msg string) { return (len(filter.Kinds) == 0 || slices.Contains(filter.Kinds, 1)) && len(filter.Authors) == 0, "an author must be specified to get their kind:1 notes" } -func NoSearchQueries(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { +func NoSearchQueries(ctx context.Context, filter *Filter) (reject bool, msg string) { if filter.Search != "" { return true, "search is not supported" } return false, "" } -func RemoveSearchQueries(ctx context.Context, filter *nostr.Filter) { +func RemoveSearchQueries(ctx Ctx, filter *Filter) { filter.Search = "" } -func RemoveAllButKinds(kinds ...uint16) func(context.Context, *nostr.Filter) { - return func(ctx context.Context, filter *nostr.Filter) { +func RemoveAllButKinds(kinds ...uint16) func(Ctx, *nostr.Filter) { + return func(ctx Ctx, filter *Filter) { if n := len(filter.Kinds); n > 0 { newKinds := make([]int, 0, n) for i := 0; i < n; i++ { @@ -62,8 +61,8 @@ func RemoveAllButKinds(kinds ...uint16) func(context.Context, *nostr.Filter) { } } -func RemoveAllButTags(tagNames ...string) func(context.Context, *nostr.Filter) { - return func(ctx context.Context, filter *nostr.Filter) { +func RemoveAllButTags(tagNames ...string) func(Ctx, *Filter) { + return func(ctx Ctx, filter *Filter) { for tagName := range filter.Tags { if !slices.Contains(tagNames, tagName) { delete(filter.Tags, tagName) diff --git a/cmd/replicatrd/replicatr/listener.go b/cmd/replicatrd/replicatr/listener.go index 22ddc466..f11aa722 100644 --- a/cmd/replicatrd/replicatr/listener.go +++ b/cmd/replicatrd/replicatr/listener.go @@ -22,7 +22,8 @@ func GetListeningFilters() (respFilters Filters) { subs.Range(func(_ string, listener *Listener) bool { for _, listenerFilter := range listener.filters { for _, respFilter := range respFilters { - // check if this filter specifically is already added to respFilters + // check if this filter specifically is already added to + // respFilters if nostr.FilterEqual(listenerFilter, respFilter) { goto next } @@ -47,8 +48,8 @@ func setListener(id string, ws *WebSocket, f Filters, c CancelCauseFunc) { subs.Store(id, &Listener{filters: f, cancel: c}) } -// remove a specific subscription id from listeners for a given ws client -// and cancel its specific context +// remove a specific subscription id from listeners for a given ws client and +// cancel its specific context func removeListenerId(ws *WebSocket, id string) { if subs, ok := listeners.Load(ws); ok { if listener, ok := subs.LoadAndDelete(id); ok { @@ -60,8 +61,8 @@ func removeListenerId(ws *WebSocket, id string) { } } -// remove WebSocket conn from listeners -// (no need to cancel contexts as they are all inherited from the main connection context) +// remove WebSocket conn from listeners (no need to cancel contexts as they are +// all inherited from the main connection context) func removeListener(ws *WebSocket) { listeners.Delete(ws) } func notifyListeners(event *Event) { diff --git a/cmd/replicatrd/replicatr/nip04.go b/cmd/replicatrd/replicatr/nip04.go new file mode 100644 index 00000000..0c501f16 --- /dev/null +++ b/cmd/replicatrd/replicatr/nip04.go @@ -0,0 +1,37 @@ +package replicatr + +import ( + "golang.org/x/exp/slices" +) + +// RejectKind04Snoopers prevents reading NIP-04 messages from people not +// involved in the conversation. +func RejectKind04Snoopers(ctx Ctx, filter *Filter) (bool, string) { + // prevent kind-4 events from being returned to unauthed users, only when + // authentication is a thing + if !slices.Contains(filter.Kinds, 4) { + return false, "" + } + ws := GetConnection(ctx) + s := filter.Authors + r, _ := filter.Tags["p"] + switch { + case ws.AuthedPublicKey == "": + // not authenticated + return true, "restricted: this relay does not serve kind-4 to " + + "unauthenticated users, does your client implement NIP-42?" + case len(s) == 1 && len(r) < 2 && (s[0] == ws.AuthedPublicKey): + // allowed filter: ws.authed is sole sender (filter specifies one or all + // r) + return false, "" + case len(r) == 1 && len(s) < 2 && (r[0] == ws.AuthedPublicKey): + // allowed filter: ws.authed is sole receiver (filter specifies one or + // all senders) + return false, "" + default: + // restricted filter: do not return any events, even if other elements + // in filters array were not restricted). client should know better. + return true, "restricted: authenticated user does not have " + + "authorization for requested filters." + } +} diff --git a/cmd/replicatrd/replicatr/relay.go b/cmd/replicatrd/replicatr/relay.go index 68bea848..817725d0 100644 --- a/cmd/replicatrd/replicatr/relay.go +++ b/cmd/replicatrd/replicatr/relay.go @@ -1,17 +1,13 @@ package replicatr import ( - "context" "net/http" "os" - "sync" "time" log2 "github.com/Hubmakerlabs/replicatr/pkg/log" "github.com/fasthttp/websocket" - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip11" "github.com/puzpuzpuz/xsync/v2" ) @@ -24,32 +20,6 @@ const ( MaxMessageSize int64 = 512000 // ??? ) -// aliases so we can swap out to another package with only here changed -type ( - Ctx = context.Context - Info = nip11.RelayInformationDocument - Event = nostr.Event - Filter = nostr.Filter - Filters = nostr.Filters - TagMap = nostr.TagMap - EventEnvelope = nostr.EventEnvelope - OKEnvelope = nostr.OKEnvelope - CountEnvelope = nostr.CountEnvelope - ClosedEnvelope = nostr.ClosedEnvelope - ReqEnvelope = nostr.ReqEnvelope - EOSEEnvelope = nostr.EOSEEnvelope - CloseEnvelope = nostr.CloseEnvelope - AuthEnvelope = nostr.AuthEnvelope - NoticeEnvelope = nostr.NoticeEnvelope - Conn = websocket.Conn - Request = http.Request - ResponseWriter = http.ResponseWriter - Mutex = sync.Mutex - WaitGroup = sync.WaitGroup - CancelCauseFunc = context.CancelCauseFunc - ListenerMap = *xsync.MapOf[string, *Listener] -) - // function types used in the relay state type ( RejectEvent func(ctx Ctx, event *Event) (reject bool, msg string)