a bunch of conversions and api tweaks on khatru and eventstore.

This commit is contained in:
fiatjaf
2025-04-17 00:08:36 -03:00
parent f7884cea4f
commit a7be696243
48 changed files with 450 additions and 576 deletions

View File

@@ -16,25 +16,25 @@ import (
//
// 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) {
func PreventTooManyIndexableTags(max int, ignoreKinds []uint16, onlyKinds []uint16) func(context.Context, nostr.Event) (bool, string) {
slices.Sort(ignoreKinds)
slices.Sort(onlyKinds)
ignore := func(kind int) bool { return false }
ignore := func(kind uint16) bool { return false }
if len(ignoreKinds) > 0 {
ignore = func(kind int) bool {
ignore = func(kind uint16) bool {
_, isIgnored := slices.BinarySearch(ignoreKinds, kind)
return isIgnored
}
}
if len(onlyKinds) > 0 {
ignore = func(kind int) bool {
ignore = func(kind uint16) bool {
_, isApplicable := slices.BinarySearch(onlyKinds, kind)
return !isApplicable
}
}
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
if ignore(event.Kind) {
return false, ""
}
@@ -53,8 +53,8 @@ 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) {
func PreventLargeTags(maxTagValueLen int) func(context.Context, nostr.Event) (bool, string) {
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
for _, tag := range event.Tags {
if len(tag) > 1 && len(tag[0]) == 1 {
if len(tag[1]) > maxTagValueLen {
@@ -68,11 +68,11 @@ 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(allowEphemeral bool, kinds ...uint16) func(context.Context, *nostr.Event) (bool, string) {
func RestrictToSpecifiedKinds(allowEphemeral bool, kinds ...uint16) func(context.Context, nostr.Event) (bool, string) {
// sort the kinds in increasing order
slices.Sort(kinds)
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
if allowEphemeral && nostr.IsEphemeralKind(event.Kind) {
return false, ""
}
@@ -85,9 +85,9 @@ func RestrictToSpecifiedKinds(allowEphemeral bool, kinds ...uint16) func(context
}
}
func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, nostr.Event) (bool, string) {
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
if nostr.Now()-event.CreatedAt > thresholdSeconds {
return true, "event too old"
}
@@ -95,9 +95,9 @@ func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, *
}
}
func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context, nostr.Event) (bool, string) {
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
return func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
if event.CreatedAt-nostr.Now() > thresholdSeconds {
return true, "event too much in the future"
}
@@ -105,12 +105,12 @@ func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context,
}
}
func RejectEventsWithBase64Media(ctx context.Context, evt *nostr.Event) (bool, string) {
func RejectEventsWithBase64Media(ctx context.Context, evt nostr.Event) (bool, string) {
return strings.Contains(evt.Content, "data:image/") || strings.Contains(evt.Content, "data:video/"), "event with base64 media"
}
func OnlyAllowNIP70ProtectedEvents(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
if nip70.IsProtected(*event) {
func OnlyAllowNIP70ProtectedEvents(ctx context.Context, event nostr.Event) (reject bool, msg string) {
if nip70.IsProtected(event) {
return false, ""
}
return true, "blocked: we only accept events protected with the nip70 \"-\" tag"

View File

@@ -4,8 +4,8 @@ import (
"context"
"slices"
"fiatjaf.com/nostr/khatru"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/khatru"
)
// NoComplexFilters disallows filters with more than 2 tags.
@@ -21,7 +21,7 @@ func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, ms
// MustAuth requires all subscribers to be authenticated
func MustAuth(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
if khatru.GetAuthed(ctx) == "" {
if _, isAuthed := khatru.GetAuthed(ctx); !isAuthed {
return true, "auth-required: all requests must be authenticated"
}
return false, ""
@@ -63,7 +63,7 @@ func RemoveSearchQueries(ctx context.Context, filter *nostr.Filter) {
func RemoveAllButKinds(kinds ...uint16) func(context.Context, *nostr.Filter) {
return func(ctx context.Context, filter *nostr.Filter) {
if n := len(filter.Kinds); n > 0 {
newKinds := make([]int, 0, n)
newKinds := make([]uint16, 0, n)
for i := 0; i < n; i++ {
if k := filter.Kinds[i]; slices.Contains(kinds, uint16(k)) {
newKinds = append(newKinds, k)

View File

@@ -7,7 +7,7 @@ import (
"fiatjaf.com/nostr"
)
func ValidateKind(ctx context.Context, evt *nostr.Event) (bool, string) {
func ValidateKind(ctx context.Context, evt nostr.Event) (bool, string) {
switch evt.Kind {
case 0:
var m struct {

47
khatru/policies/multi.go Normal file
View File

@@ -0,0 +1,47 @@
package policies
import (
"context"
"fiatjaf.com/nostr"
)
func SeqEvent(
funcs ...func(ctx context.Context, evt nostr.Event) (bool, string),
) func(context.Context, nostr.Event) (reject bool, reason string) {
return func(ctx context.Context, evt nostr.Event) (reject bool, reason string) {
for _, fn := range funcs {
reject, reason := fn(ctx, evt)
if reject {
return reject, reason
}
}
return false, ""
}
}
func SeqStore(funcs ...func(ctx context.Context, evt nostr.Event) error) func(context.Context, nostr.Event) error {
return func(ctx context.Context, evt nostr.Event) error {
for _, fn := range funcs {
err := fn(ctx, evt)
if err != nil {
return err
}
}
return nil
}
}
func SeqRequest(
funcs ...func(ctx context.Context, evt nostr.Filter) (bool, string),
) func(context.Context, nostr.Filter) (reject bool, reason string) {
return func(ctx context.Context, evt nostr.Filter) (reject bool, reason string) {
for _, fn := range funcs {
reject, reason := fn(ctx, evt)
if reject {
return reject, reason
}
}
return false, ""
}
}

View File

@@ -1,38 +0,0 @@
package policies
import (
"context"
"slices"
"fiatjaf.com/nostr/khatru"
"fiatjaf.com/nostr"
)
// 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 := khatru.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."
}
}

View File

@@ -5,14 +5,14 @@ import (
"net/http"
"time"
"fiatjaf.com/nostr/khatru"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/khatru"
)
func EventIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
func EventIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ nostr.Event) (reject bool, msg string) {
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
return func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
return func(ctx context.Context, _ nostr.Event) (reject bool, msg string) {
ip := khatru.GetIP(ctx)
if ip == "" {
return false, ""
@@ -21,11 +21,11 @@ func EventIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens
}
}
func EventPubKeyRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
func EventPubKeyRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ nostr.Event) (reject bool, msg string) {
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
return func(ctx context.Context, evt *nostr.Event) (reject bool, msg string) {
return rl(evt.PubKey), "rate-limited: slow down, please"
return func(ctx context.Context, evt nostr.Event) (reject bool, msg string) {
return rl(evt.PubKey.Hex()), "rate-limited: slow down, please"
}
}

View File

@@ -7,17 +7,15 @@ import (
)
func ApplySaneDefaults(relay *khatru.Relay) {
relay.RejectEvent = append(relay.RejectEvent,
relay.OnEvent = SeqEvent(
RejectEventsWithBase64Media,
EventIPRateLimiter(2, time.Minute*3, 10),
)
relay.RejectFilter = append(relay.RejectFilter,
relay.OnRequest = SeqRequest(
NoComplexFilters,
FilterIPRateLimiter(20, time.Minute, 100),
)
relay.RejectConnection = append(relay.RejectConnection,
ConnectionRateLimiter(1, time.Minute*5, 100),
)
relay.RejectConnection = ConnectionRateLimiter(1, time.Minute*5, 100)
}