bring in khatru and eventstore.
This commit is contained in:
117
khatru/policies/events.go
Normal file
117
khatru/policies/events.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip70"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
slices.Sort(ignoreKinds)
|
||||
slices.Sort(onlyKinds)
|
||||
|
||||
ignore := func(kind int) bool { return false }
|
||||
if len(ignoreKinds) > 0 {
|
||||
ignore = func(kind int) bool {
|
||||
_, isIgnored := slices.BinarySearch(ignoreKinds, kind)
|
||||
return isIgnored
|
||||
}
|
||||
}
|
||||
if len(onlyKinds) > 0 {
|
||||
ignore = func(kind int) bool {
|
||||
_, isApplicable := slices.BinarySearch(onlyKinds, kind)
|
||||
return !isApplicable
|
||||
}
|
||||
}
|
||||
|
||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if ignore(event.Kind) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
ntags := 0
|
||||
for _, tag := range event.Tags {
|
||||
if len(tag) > 0 && len(tag[0]) == 1 {
|
||||
ntags++
|
||||
}
|
||||
}
|
||||
if ntags > max {
|
||||
return true, "too many indexable tags"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for _, tag := range event.Tags {
|
||||
if len(tag) > 1 && len(tag[0]) == 1 {
|
||||
if len(tag[1]) > maxTagValueLen {
|
||||
return true, "event contains too large tags"
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// sort the kinds in increasing order
|
||||
slices.Sort(kinds)
|
||||
|
||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if allowEphemeral && nostr.IsEphemeralKind(event.Kind) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if _, allowed := slices.BinarySearch(kinds, uint16(event.Kind)); allowed {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, fmt.Sprintf("received event kind %d not allowed", event.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if nostr.Now()-event.CreatedAt > thresholdSeconds {
|
||||
return true, "event too old"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if event.CreatedAt-nostr.Now() > thresholdSeconds {
|
||||
return true, "event too much in the future"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return false, ""
|
||||
}
|
||||
return true, "blocked: we only accept events protected with the nip70 \"-\" tag"
|
||||
}
|
||||
93
khatru/policies/filters.go
Normal file
93
khatru/policies/filters.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
// NoComplexFilters disallows filters with more than 2 tags.
|
||||
func NoComplexFilters(ctx context.Context, filter nostr.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, ""
|
||||
}
|
||||
|
||||
// MustAuth requires all subscribers to be authenticated
|
||||
func MustAuth(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
if khatru.GetAuthed(ctx) == "" {
|
||||
return true, "auth-required: all requests must be authenticated"
|
||||
}
|
||||
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) {
|
||||
c := len(filter.Kinds) + len(filter.IDs) + len(filter.Authors)
|
||||
for _, tagItems := range filter.Tags {
|
||||
c += len(tagItems)
|
||||
}
|
||||
if c == 0 {
|
||||
return true, "can't handle empty filters"
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
if filter.Search != "" {
|
||||
return true, "search is not supported"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func RemoveSearchQueries(ctx context.Context, filter *nostr.Filter) {
|
||||
if filter.Search != "" {
|
||||
filter.Search = ""
|
||||
filter.LimitZero = true // signals that this query should be just skipped
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
for i := 0; i < n; i++ {
|
||||
if k := filter.Kinds[i]; slices.Contains(kinds, uint16(k)) {
|
||||
newKinds = append(newKinds, k)
|
||||
}
|
||||
}
|
||||
filter.Kinds = newKinds
|
||||
if len(filter.Kinds) == 0 {
|
||||
filter.LimitZero = true // signals that this query should be just skipped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveAllButTags(tagNames ...string) func(context.Context, *nostr.Filter) {
|
||||
return func(ctx context.Context, filter *nostr.Filter) {
|
||||
if n := len(filter.Tags); n > 0 {
|
||||
for tagName := range filter.Tags {
|
||||
if !slices.Contains(tagNames, tagName) {
|
||||
delete(filter.Tags, tagName)
|
||||
}
|
||||
}
|
||||
if len(filter.Tags) == 0 {
|
||||
filter.LimitZero = true // signals that this query should be just skipped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
khatru/policies/helpers.go
Normal file
42
khatru/policies/helpers.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
func startRateLimitSystem[K comparable](
|
||||
tokensPerInterval int,
|
||||
interval time.Duration,
|
||||
maxTokens int,
|
||||
) func(key K) (ratelimited bool) {
|
||||
negativeBuckets := xsync.NewMapOf[K, *atomic.Int32]()
|
||||
maxTokensInt32 := int32(maxTokens)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
for key, bucket := range negativeBuckets.Range {
|
||||
newv := bucket.Add(int32(-tokensPerInterval))
|
||||
if newv <= 0 {
|
||||
negativeBuckets.Delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func(key K) bool {
|
||||
nb, _ := negativeBuckets.LoadOrStore(key, &atomic.Int32{})
|
||||
|
||||
if nb.Load() < maxTokensInt32 {
|
||||
nb.Add(1)
|
||||
// rate limit not reached yet
|
||||
return false
|
||||
}
|
||||
|
||||
// rate limit reached
|
||||
return true
|
||||
}
|
||||
}
|
||||
29
khatru/policies/kind_validation.go
Normal file
29
khatru/policies/kind_validation.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func ValidateKind(ctx context.Context, evt *nostr.Event) (bool, string) {
|
||||
switch evt.Kind {
|
||||
case 0:
|
||||
var m struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
json.Unmarshal([]byte(evt.Content), &m)
|
||||
if m.Name == "" {
|
||||
return true, "missing json name in kind 0"
|
||||
}
|
||||
case 1:
|
||||
return false, ""
|
||||
case 2:
|
||||
return true, "this kind has been deprecated"
|
||||
}
|
||||
|
||||
// TODO: all other kinds
|
||||
|
||||
return false, ""
|
||||
}
|
||||
38
khatru/policies/nip04.go
Normal file
38
khatru/policies/nip04.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-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."
|
||||
}
|
||||
}
|
||||
46
khatru/policies/ratelimits.go
Normal file
46
khatru/policies/ratelimits.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
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) {
|
||||
ip := khatru.GetIP(ctx)
|
||||
if ip == "" {
|
||||
return false, ""
|
||||
}
|
||||
return rl(ip), "rate-limited: slow down, please"
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
func ConnectionRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(r *http.Request) bool {
|
||||
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
||||
|
||||
return func(r *http.Request) bool {
|
||||
return rl(khatru.GetIPFromRequest(r))
|
||||
}
|
||||
}
|
||||
|
||||
func FilterIPRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ nostr.Filter) (reject bool, msg string) {
|
||||
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
||||
|
||||
return func(ctx context.Context, _ nostr.Filter) (reject bool, msg string) {
|
||||
return rl(khatru.GetIP(ctx)), "rate-limited: there is a bug in the client, no one should be making so many requests"
|
||||
}
|
||||
}
|
||||
23
khatru/policies/sane_defaults.go
Normal file
23
khatru/policies/sane_defaults.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
)
|
||||
|
||||
func ApplySaneDefaults(relay *khatru.Relay) {
|
||||
relay.RejectEvent = append(relay.RejectEvent,
|
||||
RejectEventsWithBase64Media,
|
||||
EventIPRateLimiter(2, time.Minute*3, 10),
|
||||
)
|
||||
|
||||
relay.RejectFilter = append(relay.RejectFilter,
|
||||
NoComplexFilters,
|
||||
FilterIPRateLimiter(20, time.Minute, 100),
|
||||
)
|
||||
|
||||
relay.RejectConnection = append(relay.RejectConnection,
|
||||
ConnectionRateLimiter(1, time.Minute*5, 100),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user