mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-07 14:06:51 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f54a7bf3 | ||
|
|
e03a02fed7 | ||
|
|
255f7bc827 | ||
|
|
3214dac302 | ||
|
|
5efadf6256 | ||
|
|
27d6769009 | ||
|
|
44baacac42 | ||
|
|
35053f6215 | ||
|
|
8854ad7a95 | ||
|
|
c5c17029ba | ||
|
|
e174dd6a95 | ||
|
|
cd4c25c845 | ||
|
|
9b43da0b17 | ||
|
|
e9bcad8614 |
@@ -69,6 +69,11 @@ func main() {
|
||||
|
||||
// there are many other configurable things you can set
|
||||
relay.RejectEvent = append(relay.RejectEvent,
|
||||
// built-in policies
|
||||
policies.ValidateKind,
|
||||
|
||||
// define your own policies
|
||||
policies.PreventLargeTags(80),
|
||||
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
||||
return true, "we don't allow this person to write here"
|
||||
@@ -79,6 +84,10 @@ func main() {
|
||||
|
||||
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
||||
relay.RejectFilter = append(relay.RejectFilter,
|
||||
// built-in policies
|
||||
policies.NoComplexFilters,
|
||||
|
||||
// define your own policies
|
||||
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
|
||||
log.Printf("request from %s\n", pubkey)
|
||||
|
||||
46
adding.go
46
adding.go
@@ -31,29 +31,53 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) error {
|
||||
oee(ctx, evt)
|
||||
}
|
||||
} else {
|
||||
// will store
|
||||
|
||||
// but first check if we already have it
|
||||
filter := nostr.Filter{IDs: []string{evt.ID}}
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, filter)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for range ch {
|
||||
// if we run this it means we already have this event, so we just return a success and exit
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if it's replaceable we first delete old versions
|
||||
if evt.Kind == 0 || evt.Kind == 3 || (10000 <= evt.Kind && evt.Kind < 20000) {
|
||||
// replaceable event, delete before storing
|
||||
filter := nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}}
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}})
|
||||
ch, err := query(ctx, filter)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if previous := <-ch; previous != nil && isOlder(previous, evt) {
|
||||
for _, del := range rl.DeleteEvent {
|
||||
del(ctx, previous)
|
||||
for previous := range ch {
|
||||
if isOlder(previous, evt) {
|
||||
for _, del := range rl.DeleteEvent {
|
||||
del(ctx, previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if 30000 <= evt.Kind && evt.Kind < 40000 {
|
||||
// parameterized replaceable event, delete before storing
|
||||
d := evt.Tags.GetFirst([]string{"d", ""})
|
||||
if d != nil {
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}, Tags: nostr.TagMap{"d": []string{d.Value()}}})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if previous := <-ch; previous != nil && isOlder(previous, evt) {
|
||||
if d == nil {
|
||||
return fmt.Errorf("invalid: missing 'd' tag on parameterized replaceable event")
|
||||
}
|
||||
|
||||
filter := nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}, Tags: nostr.TagMap{"d": []string{(*d)[1]}}}
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, filter)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for previous := range ch {
|
||||
if isOlder(previous, evt) {
|
||||
for _, del := range rl.DeleteEvent {
|
||||
del(ctx, previous)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) erro
|
||||
if acceptDeletion {
|
||||
// delete it
|
||||
for _, del := range rl.DeleteEvent {
|
||||
del(ctx, target)
|
||||
if err := del(ctx, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fail and stop here
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/eventstore/elasticsearch"
|
||||
"github.com/fiatjaf/khatru"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/eventstore/postgresql"
|
||||
"github.com/fiatjaf/khatru"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/eventstore/sqlite3"
|
||||
"github.com/fiatjaf/khatru"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/policies"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
@@ -53,6 +54,11 @@ func main() {
|
||||
|
||||
// there are many other configurable things you can set
|
||||
relay.RejectEvent = append(relay.RejectEvent,
|
||||
// built-in policies
|
||||
policies.ValidateKind,
|
||||
|
||||
// define your own policies
|
||||
policies.PreventLargeTags(80),
|
||||
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
||||
return true, "we don't allow this person to write here"
|
||||
@@ -63,6 +69,10 @@ func main() {
|
||||
|
||||
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
||||
relay.RejectFilter = append(relay.RejectFilter,
|
||||
// built-in policies
|
||||
policies.NoComplexFilters,
|
||||
|
||||
// define your own policies
|
||||
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
|
||||
log.Printf("request from %s\n", pubkey)
|
||||
|
||||
4
go.mod
4
go.mod
@@ -5,11 +5,10 @@ go 1.21.4
|
||||
require (
|
||||
github.com/fasthttp/websocket v1.5.7
|
||||
github.com/fiatjaf/eventstore v0.3.8
|
||||
github.com/nbd-wtf/go-nostr v0.28.1
|
||||
github.com/nbd-wtf/go-nostr v0.30.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -51,6 +50,7 @@ require (
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -115,6 +115,8 @@ github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+
|
||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/nbd-wtf/go-nostr v0.28.1 h1:XQi/lBsigBXHRm7IDBJE7SR9citCh9srgf8sA5iVW3A=
|
||||
github.com/nbd-wtf/go-nostr v0.28.1/go.mod h1:OQ8sNLFJnsj17BdqZiLSmjJBIFTfDqckEYC3utS4qoY=
|
||||
github.com/nbd-wtf/go-nostr v0.30.0 h1:rN085pe4IxmSBVht8LChZbWLggonjA8hPIk8l4/+Hjk=
|
||||
github.com/nbd-wtf/go-nostr v0.30.0/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
25
handlers.go
25
handlers.go
@@ -134,6 +134,31 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// check NIP-70 protected
|
||||
for _, v := range env.Event.Tags {
|
||||
if len(v) == 1 && v[0] == "-" {
|
||||
msg := "must be published by event author"
|
||||
authed := GetAuthed(ctx)
|
||||
if authed == "" {
|
||||
RequestAuth(ctx)
|
||||
ws.WriteJSON(nostr.OKEnvelope{
|
||||
EventID: env.Event.ID,
|
||||
OK: false,
|
||||
Reason: "auth-required: " + msg,
|
||||
})
|
||||
return
|
||||
}
|
||||
if authed != env.Event.PubKey {
|
||||
ws.WriteJSON(nostr.OKEnvelope{
|
||||
EventID: env.Event.ID,
|
||||
OK: false,
|
||||
Reason: "blocked: " + msg,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ok bool
|
||||
var writeErr error
|
||||
if env.Event.Kind == 5 {
|
||||
|
||||
@@ -2,9 +2,10 @@ package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"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
|
||||
@@ -13,6 +14,9 @@ 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) {
|
||||
slices.Sort(ignoreKinds)
|
||||
slices.Sort(onlyKinds)
|
||||
|
||||
ignore := func(kind int) bool { return false }
|
||||
if len(ignoreKinds) > 0 {
|
||||
ignore = func(kind int) bool {
|
||||
@@ -73,21 +77,25 @@ func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Even
|
||||
}
|
||||
}
|
||||
|
||||
// sort the kinds in increasing order
|
||||
slices.Sort(kinds)
|
||||
|
||||
return func(ctx context.Context, event *nostr.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 {
|
||||
return true, "event kind not allowed"
|
||||
return true, fmt.Sprintf("event kind not allowed (it should be lower than %d)", max)
|
||||
}
|
||||
if event.Kind < min {
|
||||
return true, "event kind not allowed"
|
||||
return true, fmt.Sprintf("event kind not allowed (it should be higher than %d)", min)
|
||||
}
|
||||
|
||||
// hopefully this map of uint16s is very fast
|
||||
if _, allowed := slices.BinarySearch(kinds, uint16(event.Kind)); allowed {
|
||||
return false, ""
|
||||
}
|
||||
return true, "event kind not allowed"
|
||||
|
||||
return true, fmt.Sprintf("received event kind %d not allowed", event.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// NoComplexFilters disallows filters with more than 2 tags.
|
||||
@@ -45,7 +45,10 @@ func NoSearchQueries(ctx context.Context, filter nostr.Filter) (reject bool, msg
|
||||
}
|
||||
|
||||
func RemoveSearchQueries(ctx context.Context, filter *nostr.Filter) {
|
||||
filter.Search = ""
|
||||
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) {
|
||||
@@ -58,15 +61,23 @@ func RemoveAllButKinds(kinds ...uint16) func(context.Context, *nostr.Filter) {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
for tagName := range filter.Tags {
|
||||
if !slices.Contains(tagNames, tagName) {
|
||||
delete(filter.Tags, tagName)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
policies/kind_validation.go
Normal file
29
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, ""
|
||||
}
|
||||
@@ -3,9 +3,10 @@ package policies
|
||||
import (
|
||||
"context"
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// RejectKind04Snoopers prevents reading NIP-04 messages from people not involved in the conversation.
|
||||
|
||||
2
relay.go
2
relay.go
@@ -20,7 +20,7 @@ func NewRelay() *Relay {
|
||||
Info: &nip11.RelayInformationDocument{
|
||||
Software: "https://github.com/fiatjaf/khatru",
|
||||
Version: "n/a",
|
||||
SupportedNIPs: make([]int, 0),
|
||||
SupportedNIPs: []int{1, 11, 70},
|
||||
},
|
||||
|
||||
upgrader: websocket.Upgrader{
|
||||
|
||||
@@ -17,9 +17,8 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
||||
ovw(ctx, &filter)
|
||||
}
|
||||
|
||||
if filter.Limit < 0 {
|
||||
// this is a special situation through which the implementor signals to us that it doesn't want
|
||||
// to event perform any queries whatsoever
|
||||
if filter.LimitZero {
|
||||
// don't do any queries, just subscribe to future events
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +28,6 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
||||
// filter we can just reject it)
|
||||
for _, reject := range rl.RejectFilter {
|
||||
if reject, msg := reject(ctx, filter); reject {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user