Compare commits

..

1 Commits

36 changed files with 765 additions and 1594 deletions

View File

@@ -1,14 +0,0 @@
name: test every commit
on:
- push
- pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v3
with:
go-version-file: ./go.mod
- run: go test ./...

141
README.md
View File

@@ -1,140 +1 @@
# khatru, a relay framework [![docs badge](https://img.shields.io/badge/docs-reference-blue)](https://pkg.go.dev/github.com/fiatjaf/khatru#Relay)
[![Run Tests](https://github.com/fiatjaf/khatru/actions/workflows/test.yml/badge.svg)](https://github.com/fiatjaf/khatru/actions/workflows/test.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/fiatjaf/khatru.svg)](https://pkg.go.dev/github.com/fiatjaf/khatru)
[![Go Report Card](https://goreportcard.com/badge/github.com/fiatjaf/khatru)](https://goreportcard.com/report/github.com/fiatjaf/khatru)
Khatru makes it easy to write very very custom relays:
- custom event or filter acceptance policies
- custom `AUTH` handlers
- custom storage and pluggable databases
- custom webpages and other HTTP handlers
Here's a sample:
```go
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/fiatjaf/khatru"
"github.com/nbd-wtf/go-nostr"
)
func main() {
// create the relay instance
relay := khatru.NewRelay()
// set up some basic properties (will be returned on the NIP-11 endpoint)
relay.Info.Name = "my relay"
relay.Info.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
relay.Info.Description = "this is my custom relay"
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
// you must bring your own storage scheme -- if you want to have any
store := make(map[string]*nostr.Event, 120)
// set up the basic relay functions
relay.StoreEvent = append(relay.StoreEvent,
func(ctx context.Context, event *nostr.Event) error {
store[event.ID] = event
return nil
},
)
relay.QueryEvents = append(relay.QueryEvents,
func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
ch := make(chan *nostr.Event)
go func() {
for _, evt := range store {
if filter.Matches(evt) {
ch <- evt
}
}
close(ch)
}()
return ch, nil
},
)
relay.DeleteEvent = append(relay.DeleteEvent,
func(ctx context.Context, event *nostr.Event) error {
delete(store, event.ID)
return nil
},
)
// 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"
}
return false, "" // anyone else can
},
)
// 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)
return false, ""
}
return true, "auth-required: only authenticated users can read from this relay"
// (this will cause an AUTH message to be sent and then a CLOSED message such that clients can
// authenticate and then request again)
},
)
// check the docs for more goodies!
mux := relay.Router()
// set up other http handlers
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "text/html")
fmt.Fprintf(w, `<b>welcome</b> to my relay!`)
})
// start the server
fmt.Println("running on :3334")
http.ListenAndServe(":3334", relay)
}
```
### But I don't want to write my own database!
Fear no more. Using the https://github.com/fiatjaf/eventstore module you get a bunch of compatible databases out of the box and you can just plug them into your relay. For example, [sqlite](https://pkg.go.dev/github.com/fiatjaf/eventstore/sqlite3):
```go
db := sqlite3.SQLite3Backend{DatabaseURL: "/tmp/khatru-sqlite-tmp"}
if err := db.Init(); err != nil {
panic(err)
}
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
```
### But I don't want to write a bunch of custom policies!
Fear no more. We have a bunch of common policies written in the `github.com/fiatjaf/khatru/policies` package and also a handpicked selection of base sane defaults, which you can apply with:
```go
policies.ApplySaneDefaults(relay)
```
Contributions to this are very much welcomed.
khatru

138
add-event.go Normal file
View File

@@ -0,0 +1,138 @@
package khatru
import (
"context"
"fmt"
"github.com/nbd-wtf/go-nostr"
)
func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) error {
if evt == nil {
return fmt.Errorf("event is nil")
}
msg := ""
rejecting := false
for _, reject := range rl.RejectEvent {
rejecting, msg = reject(ctx, evt)
if rejecting {
break
}
}
if rejecting {
if msg == "" {
msg = "no reason"
}
return fmt.Errorf(msg)
}
if 20000 <= evt.Kind && evt.Kind < 30000 {
// do not store ephemeral events
} else {
if evt.Kind == 0 || evt.Kind == 3 || (10000 <= evt.Kind && evt.Kind < 20000) {
// replaceable event, delete before storing
for _, query := range rl.QueryEvents {
ch, err := query(ctx, nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}})
if err != nil {
continue
}
previous := <-ch
if previous != nil {
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
}
previous := <-ch
if previous != nil {
for _, del := range rl.DeleteEvent {
del(ctx, previous)
}
}
}
}
}
// store
for _, store := range rl.StoreEvent {
if saveErr := store(ctx, evt); saveErr != nil {
switch saveErr {
case ErrDupEvent:
return nil
default:
errmsg := saveErr.Error()
if nip20prefixmatcher.MatchString(errmsg) {
return saveErr
} else {
return fmt.Errorf("error: failed to save (%s)", errmsg)
}
}
}
}
for _, ons := range rl.OnEventSaved {
ons(ctx, evt)
}
}
for _, ovw := range rl.OverwriteResponseEvent {
ovw(ctx, evt)
}
notifyListeners(evt)
return nil
}
func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) error {
// event deletion -- nip09
for _, tag := range evt.Tags {
if len(tag) >= 2 && tag[0] == "e" {
// first we fetch the event
for _, query := range rl.QueryEvents {
ch, err := query(ctx, nostr.Filter{IDs: []string{tag[1]}})
if err != nil {
continue
}
target := <-ch
if target == nil {
continue
}
// got the event, now check if the user can delete it
acceptDeletion := target.PubKey == evt.PubKey
var msg string
if acceptDeletion == false {
msg = "you are not the author of this event"
}
// but if we have a function to overwrite this outcome, use that instead
for _, odo := range rl.OverwriteDeletionOutcome {
acceptDeletion, msg = odo(ctx, target, evt)
}
if acceptDeletion {
// delete it
for _, del := range rl.DeleteEvent {
del(ctx, target)
}
} else {
// fail and stop here
return fmt.Errorf("blocked: %s", msg)
}
// don't try to query this same event again
break
}
}
}
return nil
}

107
adding.go
View File

@@ -1,107 +0,0 @@
package khatru
import (
"context"
"errors"
"fmt"
"github.com/fiatjaf/eventstore"
"github.com/nbd-wtf/go-nostr"
)
// AddEvent sends an event through then normal add pipeline, as if it was received from a websocket.
func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast bool, writeError error) {
if evt == nil {
return false, errors.New("error: event is nil")
}
for _, reject := range rl.RejectEvent {
if reject, msg := reject(ctx, evt); reject {
if msg == "" {
return false, errors.New("blocked: no reason")
} else {
return false, errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
}
}
}
if 20000 <= evt.Kind && evt.Kind < 30000 {
// do not store ephemeral events
for _, oee := range rl.OnEphemeralEvent {
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 true, 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, filter)
if err != nil {
continue
}
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 {
return false, 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)
}
}
}
}
}
// store
for _, store := range rl.StoreEvent {
if saveErr := store(ctx, evt); saveErr != nil {
switch saveErr {
case eventstore.ErrDupEvent:
return true, nil
default:
return false, fmt.Errorf(nostr.NormalizeOKMessage(saveErr.Error(), "error"))
}
}
}
for _, ons := range rl.OnEventSaved {
ons(ctx, evt)
}
}
return false, nil
}

View File

@@ -1,11 +0,0 @@
package khatru
import (
"github.com/nbd-wtf/go-nostr"
)
// BroadcastEvent emits an event to all listeners whose filters' match, skipping all filters and actions
// it also doesn't attempt to store the event or trigger any reactions or callbacks
func (rl *Relay) BroadcastEvent(evt *nostr.Event) {
notifyListeners(evt)
}

View File

@@ -1,53 +0,0 @@
package khatru
import (
"context"
"fmt"
"github.com/nbd-wtf/go-nostr"
)
func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) error {
// event deletion -- nip09
for _, tag := range evt.Tags {
if len(tag) >= 2 && tag[0] == "e" {
// first we fetch the event
for _, query := range rl.QueryEvents {
ch, err := query(ctx, nostr.Filter{IDs: []string{tag[1]}})
if err != nil {
continue
}
target := <-ch
if target == nil {
continue
}
// got the event, now check if the user can delete it
acceptDeletion := target.PubKey == evt.PubKey
var msg string
if acceptDeletion == false {
msg = "you are not the author of this event"
}
// but if we have a function to overwrite this outcome, use that instead
for _, odo := range rl.OverwriteDeletionOutcome {
acceptDeletion, msg = odo(ctx, target, evt)
}
if acceptDeletion {
// delete it
for _, del := range rl.DeleteEvent {
if err := del(ctx, target); err != nil {
return err
}
}
} else {
// fail and stop here
return fmt.Errorf("blocked: %s", msg)
}
// don't try to query this same event again
break
}
}
}
return nil
}

5
errors.go Normal file
View File

@@ -0,0 +1,5 @@
package khatru
import "fmt"
var ErrDupEvent = fmt.Errorf("duplicate: event already exists")

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"net/http"
"github.com/fiatjaf/eventstore/elasticsearch"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/eventstore/elasticsearch"
)
func main() {

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"net/http"
"github.com/fiatjaf/eventstore/postgresql"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/eventstore/postgresql"
)
func main() {

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"net/http"
"github.com/fiatjaf/eventstore/sqlite3"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/eventstore/sqlite3"
)
func main() {

View File

@@ -8,7 +8,7 @@ import (
"github.com/fiatjaf/eventstore/lmdb"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/policies"
"github.com/fiatjaf/khatru/plugins"
"github.com/nbd-wtf/go-nostr"
)
@@ -26,8 +26,8 @@ func main() {
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
relay.RejectEvent = append(relay.RejectEvent, policies.PreventTooManyIndexableTags(10, nil, nil))
relay.RejectFilter = append(relay.RejectFilter, policies.NoComplexFilters)
relay.RejectEvent = append(relay.RejectEvent, plugins.PreventTooManyIndexableTags(10))
relay.RejectFilter = append(relay.RejectFilter, plugins.NoPrefixFilters, plugins.NoComplexFilters)
relay.OnEventSaved = append(relay.OnEventSaved, func(ctx context.Context, event *nostr.Event) {
})

Binary file not shown.

View File

@@ -1,98 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/policies"
"github.com/nbd-wtf/go-nostr"
)
func main() {
// create the relay instance
relay := khatru.NewRelay()
// set up some basic properties (will be returned on the NIP-11 endpoint)
relay.Info.Name = "my relay"
relay.Info.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
relay.Info.Description = "this is my custom relay"
relay.Info.Icon = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fliquipedia.net%2Fcommons%2Fimages%2F3%2F35%2FSCProbe.jpg&f=1&nofb=1&ipt=0cbbfef25bce41da63d910e86c3c343e6c3b9d63194ca9755351bb7c2efa3359&ipo=images"
// you must bring your own storage scheme -- if you want to have any
store := make(map[string]*nostr.Event, 120)
// set up the basic relay functions
relay.StoreEvent = append(relay.StoreEvent,
func(ctx context.Context, event *nostr.Event) error {
store[event.ID] = event
return nil
},
)
relay.QueryEvents = append(relay.QueryEvents,
func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error) {
ch := make(chan *nostr.Event)
go func() {
for _, evt := range store {
if filter.Matches(evt) {
ch <- evt
}
}
close(ch)
}()
return ch, nil
},
)
relay.DeleteEvent = append(relay.DeleteEvent,
func(ctx context.Context, event *nostr.Event) error {
delete(store, event.ID)
return nil
},
)
// 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"
}
return false, "" // anyone else can
},
)
// 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)
return false, ""
}
return true, "auth-required: only authenticated users can read from this relay"
// (this will cause an AUTH message to be sent and then a CLOSED message such that clients can
// authenticate and then request again)
},
)
// check the docs for more goodies!
mux := relay.Router()
// set up other http handlers
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "text/html")
fmt.Fprintf(w, `<b>welcome</b> to my relay!`)
})
// start the server
fmt.Println("running on :3334")
http.ListenAndServe(":3334", relay)
}

48
go.mod
View File

@@ -1,19 +1,21 @@
module github.com/fiatjaf/khatru
go 1.21.4
go 1.21.0
require (
github.com/fasthttp/websocket v1.5.7
github.com/fiatjaf/eventstore v0.5.1
github.com/nbd-wtf/go-nostr v0.34.3
github.com/puzpuzpuz/xsync/v3 v3.0.2
github.com/fasthttp/websocket v1.5.3
github.com/fiatjaf/eventstore v0.1.0
github.com/gobwas/ws v1.2.0
github.com/nbd-wtf/go-nostr v0.25.1
github.com/puzpuzpuz/xsync/v2 v2.5.1
github.com/rs/cors v1.7.0
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
)
require (
github.com/PowerDNS/lmdb-go v1.9.2 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/aquasecurity/esquery v0.2.0 // indirect
github.com/bmatsuo/lmdb-go v1.8.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
@@ -21,36 +23,34 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect
github.com/elastic/go-elasticsearch/v7 v7.17.10 // indirect
github.com/elastic/go-elasticsearch/v7 v7.6.0 // indirect
github.com/elastic/go-elasticsearch/v8 v8.10.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v1.14.18 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
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.20.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
github.com/valyala/fasthttp v1.47.0 // indirect
go.opencensus.io v0.22.5 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

147
go.sum
View File

@@ -1,21 +1,19 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PowerDNS/lmdb-go v1.9.2 h1:Cmgerh9y3ZKBZGz1irxSShhfmFyRUh+Zdk4cZk7ZJvU=
github.com/PowerDNS/lmdb-go v1.9.2/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aquasecurity/esquery v0.2.0 h1:9WWXve95TE8hbm3736WB7nS6Owl8UGDeu+0jiyE9ttA=
github.com/aquasecurity/esquery v0.2.0/go.mod h1:VU+CIFR6C+H142HHZf9RUkp4Eedpo9UrEKeCQHWf9ao=
github.com/bmatsuo/lmdb-go v1.8.0 h1:ohf3Q4xjXZBKh4AayUY4bb2CXuhRAI8BYGlJq08EfNA=
github.com/bmatsuo/lmdb-go v1.8.0/go.mod h1:wWPZmKdOAZsl4qOqkowQ1aCrFie1HU8gWloHMCeAUdM=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -29,70 +27,49 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elastic/elastic-transport-go/v8 v8.3.0 h1:DJGxovyQLXGr62e9nDMPSxRyWION0Bh6d9eCFBriiHo=
github.com/elastic/elastic-transport-go/v8 v8.3.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI=
github.com/elastic/go-elasticsearch/v7 v7.6.0 h1:sYpGLpEFHgLUKLsZUBfuaVI9QgHjS3JdH9fX4/z8QI8=
github.com/elastic/go-elasticsearch/v7 v7.6.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
github.com/elastic/go-elasticsearch/v7 v7.17.10 h1:TCQ8i4PmIJuBunvBS6bwT2ybzVFxxUhhltAs3Gyu1yo=
github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4=
github.com/elastic/go-elasticsearch/v8 v8.10.1 h1:JJ3i2DimYTsJcUoEGbg6tNB0eehTNdid9c5kTR1TGuI=
github.com/elastic/go-elasticsearch/v8 v8.10.1/go.mod h1:GU1BJHO7WeamP7UhuElYwzzHtvf9SDmeVpSSy9+o6Qg=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4=
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fiatjaf/eventstore v0.5.1 h1:tTh+JYP0RME51VY2QB2Gvtzj6QTaZAnSVhgZtrYqY2A=
github.com/fiatjaf/eventstore v0.5.1/go.mod h1:r5yCFmrVNT2b1xUOuMnDVS3xPGh97y8IgTcLyY2rYP8=
github.com/fiatjaf/eventstore v0.1.0 h1:/g7VTw6dsXmjICD3rBuHNIvAammHJ5unrKJ71Dz+VTs=
github.com/fiatjaf/eventstore v0.1.0/go.mod h1:juMei5HL3HJi6t7vZjj7VdEItDPu31+GLROepdUK4tw=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jgroeneveld/schema v1.0.0 h1:J0E10CrOkiSEsw6dfb1IfrDJD14pf6QLVJ3tRPl/syI=
github.com/jgroeneveld/schema v1.0.0/go.mod h1:M14lv7sNMtGvo3ops1MwslaSYgDYxrSmbzWIQ0Mr5rs=
github.com/jgroeneveld/trial v2.0.0+incompatible h1:d59ctdgor+VqdZCAiUfVN8K13s0ALDioG5DWwZNtRuQ=
@@ -103,59 +80,52 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/nbd-wtf/go-nostr v0.34.3 h1:JfDOHOje7gzUhisbZD0v2Y9b9vh2PmP6eHsU/GfU8QE=
github.com/nbd-wtf/go-nostr v0.34.3/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/nbd-wtf/go-nostr v0.25.1 h1:YTLTDUgngfzd3qQ0fWmQmq20flwnGtHH0g0Q8S3HlW4=
github.com/nbd-wtf/go-nostr v0.25.1/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
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=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -169,30 +139,30 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -203,30 +173,15 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -5,42 +5,30 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"encoding/json"
"net/http"
"strings"
"sync"
"time"
"github.com/fasthttp/websocket"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip11"
"github.com/nbd-wtf/go-nostr/nip42"
"github.com/rs/cors"
)
// ServeHTTP implements http.Handler interface.
func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if rl.ServiceURL == "" {
rl.ServiceURL = getServiceBaseURL(r)
}
if r.Header.Get("Upgrade") == "websocket" {
rl.HandleWebsocket(w, r)
} else if r.Header.Get("Accept") == "application/nostr+json" {
cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP11)).ServeHTTP(w, r)
} else if r.Header.Get("Content-Type") == "application/nostr+json+rpc" {
cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP86)).ServeHTTP(w, r)
rl.HandleNIP11(w, r)
} else {
rl.serveMux.ServeHTTP(w, r)
}
}
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
for _, reject := range rl.RejectConnection {
if reject(r) {
w.WriteHeader(429) // Too many requests
return
}
}
ctx := r.Context()
conn, err := rl.upgrader.Upgrade(w, r, nil)
if err != nil {
@@ -55,34 +43,21 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
rand.Read(challenge)
ws := &WebSocket{
conn: conn,
Request: r,
Challenge: hex.EncodeToString(challenge),
}
ctx, cancel := context.WithCancel(
context.WithValue(
context.Background(),
wsKey, ws,
),
)
kill := func() {
for _, ondisconnect := range rl.OnDisconnect {
ondisconnect(ctx)
}
ticker.Stop()
cancel()
if _, ok := rl.clients.Load(conn); ok {
conn.Close()
rl.clients.Delete(conn)
removeListener(ws)
}
conn: conn,
Challenge: hex.EncodeToString(challenge),
WaitingForAuth: make(chan struct{}),
}
// reader
go func() {
defer kill()
defer func() {
ticker.Stop()
if _, ok := rl.clients.Load(conn); ok {
conn.Close()
rl.clients.Delete(conn)
removeListener(ws)
}
}()
conn.SetReadLimit(rl.MaxMessageSize)
conn.SetReadDeadline(time.Now().Add(rl.PongWait))
@@ -100,15 +75,13 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
if err != nil {
if websocket.IsUnexpectedCloseError(
err,
websocket.CloseNormalClosure, // 1000
websocket.CloseGoingAway, // 1001
websocket.CloseNoStatusReceived, // 1005
websocket.CloseAbnormalClosure, // 1006
4537, // some client seems to send many of these
) {
rl.Log.Printf("unexpected close error from %s: %v\n", r.Header.Get("X-Forwarded-For"), err)
}
return
break
}
if typ == websocket.PingMessage {
@@ -117,164 +90,246 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
}
go func(message []byte) {
envelope := nostr.ParseMessage(message)
if envelope == nil {
ctx = context.Background()
var request []json.RawMessage
if err := json.Unmarshal(message, &request); err != nil {
// stop silently
return
}
switch env := envelope.(type) {
case *nostr.EventEnvelope:
// check id
hash := sha256.Sum256(env.Event.Serialize())
id := hex.EncodeToString(hash[:])
if id != env.Event.ID {
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: id is computed incorrectly"})
if len(request) < 2 {
ws.WriteJSON(nostr.NoticeEnvelope("request has less than 2 parameters"))
return
}
var typ string
json.Unmarshal(request[0], &typ)
switch typ {
case "EVENT":
// it's a new event
var evt nostr.Event
if err := json.Unmarshal(request[1], &evt); err != nil {
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode event: " + err.Error()))
return
}
// check signature
if ok, err := env.Event.CheckSignature(); err != nil {
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to verify signature"})
// check serialization
serialized := evt.Serialize()
// assign ID
hash := sha256.Sum256(serialized)
evt.ID = hex.EncodeToString(hash[:])
// check signature (requires the ID to be set)
if ok, err := evt.CheckSignature(); err != nil {
reason := "error: failed to verify signature"
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: &reason})
return
} else if !ok {
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: signature is invalid"})
reason := "invalid: signature is invalid"
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: &reason})
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
var skipBroadcast bool
if env.Event.Kind == 5 {
// this always returns "blocked: " whenever it returns an error
writeErr = rl.handleDeleteRequest(ctx, &env.Event)
if evt.Kind == 5 {
err = rl.handleDeleteRequest(ctx, &evt)
} else {
// this will also always return a prefixed reason
skipBroadcast, writeErr = rl.AddEvent(ctx, &env.Event)
err = rl.AddEvent(ctx, &evt)
}
var reason string
if writeErr == nil {
var reason *string
if err == nil {
ok = true
for _, ovw := range rl.OverwriteResponseEvent {
ovw(ctx, &env.Event)
}
if !skipBroadcast {
notifyListeners(&env.Event)
}
} else {
reason = writeErr.Error()
if strings.HasPrefix(reason, "auth-required:") {
RequestAuth(ctx)
}
msg := err.Error()
reason = &msg
}
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: ok, Reason: reason})
case *nostr.CountEnvelope:
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: ok, Reason: reason})
case "COUNT":
if rl.CountEvents == nil {
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: "unsupported: this relay does not support NIP-45"})
ws.WriteJSON(nostr.NoticeEnvelope("this relay does not support NIP-45"))
return
}
var total int64
for _, filter := range env.Filters {
total += rl.handleCountRequest(ctx, ws, filter)
var id string
json.Unmarshal(request[1], &id)
if id == "" {
ws.WriteJSON(nostr.NoticeEnvelope("COUNT has no <id>"))
return
}
ws.WriteJSON(nostr.CountEnvelope{SubscriptionID: env.SubscriptionID, Count: &total})
case *nostr.ReqEnvelope:
eose := sync.WaitGroup{}
eose.Add(len(env.Filters))
// a context just for the "stored events" request handler
reqCtx, cancelReqCtx := context.WithCancelCause(ctx)
// expose subscription id in the context
reqCtx = context.WithValue(reqCtx, subscriptionIdKey, env.SubscriptionID)
// handle each filter separately -- dispatching events as they're loaded from databases
for _, filter := range env.Filters {
err := rl.handleRequest(reqCtx, env.SubscriptionID, &eose, ws, filter)
if err != nil {
// fail everything if any filter is rejected
reason := err.Error()
if strings.HasPrefix(reason, "auth-required:") {
RequestAuth(ctx)
}
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: reason})
cancelReqCtx(errors.New("filter rejected"))
return
var total int64
filters := make(nostr.Filters, len(request)-2)
for i, filterReq := range request[2:] {
if err := json.Unmarshal(filterReq, &filters[i]); err != nil {
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
continue
}
filter := filters[i]
for _, reject := range rl.RejectFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
continue
}
}
for _, reject := range rl.RejectCountFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
continue
}
}
for _, count := range rl.CountEvents {
res, err := count(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
}
total += res
}
}
ws.WriteJSON([]interface{}{"COUNT", id, map[string]int64{"count": total}})
case "REQ":
var id string
json.Unmarshal(request[1], &id)
if id == "" {
ws.WriteJSON(nostr.NoticeEnvelope("REQ has no <id>"))
return
}
filters := make(nostr.Filters, len(request)-2)
eose := sync.WaitGroup{}
eose.Add(len(request[2:]))
for i, filterReq := range request[2:] {
if err := json.Unmarshal(
filterReq,
&filters[i],
); err != nil {
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
eose.Done()
continue
}
filter := filters[i]
for _, reject := range rl.RejectCountFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
eose.Done()
continue
}
}
eose.Add(len(rl.QueryEvents))
for _, query := range rl.QueryEvents {
ch, err := query(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
eose.Done()
continue
}
go func(ch chan *nostr.Event) {
for event := range ch {
for _, ovw := range rl.OverwriteResponseEvent {
ovw(ctx, event)
}
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
}
eose.Done()
}(ch)
}
eose.Done()
}
go func() {
// when all events have been loaded from databases and dispatched
// we can cancel the context and fire the EOSE message
eose.Wait()
cancelReqCtx(nil)
ws.WriteJSON(nostr.EOSEEnvelope(env.SubscriptionID))
ws.WriteJSON(nostr.EOSEEnvelope(id))
}()
setListener(env.SubscriptionID, ws, env.Filters, cancelReqCtx)
case *nostr.CloseEnvelope:
removeListenerId(ws, string(*env))
case *nostr.AuthEnvelope:
wsBaseUrl := strings.Replace(rl.ServiceURL, "http", "ws", 1)
if pubkey, ok := nip42.ValidateAuthEvent(&env.Event, ws.Challenge, wsBaseUrl); ok {
ws.AuthedPublicKey = pubkey
ws.authLock.Lock()
if ws.Authed != nil {
close(ws.Authed)
ws.Authed = nil
setListener(id, ws, filters)
case "CLOSE":
var id string
json.Unmarshal(request[1], &id)
if id == "" {
ws.WriteJSON(nostr.NoticeEnvelope("CLOSE has no <id>"))
return
}
removeListenerId(ws, id)
case "AUTH":
if rl.ServiceURL != "" {
var evt nostr.Event
if err := json.Unmarshal(request[1], &evt); err != nil {
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode auth event: " + err.Error()))
return
}
if pubkey, ok := nip42.ValidateAuthEvent(&evt, ws.Challenge, rl.ServiceURL); ok {
ws.Authed = pubkey
close(ws.WaitingForAuth)
ctx = context.WithValue(ctx, AUTH_CONTEXT_KEY, pubkey)
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: true})
} else {
reason := "error: failed to authenticate"
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: &reason})
}
ws.authLock.Unlock()
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: true})
} else {
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to authenticate"})
}
}
}(message)
}
}()
// writer
go func() {
defer kill()
defer func() {
ticker.Stop()
conn.Close()
}()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
err := ws.WriteMessage(websocket.PingMessage, nil)
if err != nil {
if !strings.HasSuffix(err.Error(), "use of closed network connection") {
rl.Log.Printf("error writing ping: %v; closing websocket\n", err)
}
rl.Log.Printf("error writing ping: %v; closing websocket\n", err)
return
}
}
}
}()
}
func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
supportedNIPs := []int{9, 11, 12, 15, 16, 20, 33}
if rl.ServiceURL != "" {
supportedNIPs = append(supportedNIPs, 42)
}
if rl.CountEvents != nil {
supportedNIPs = append(supportedNIPs, 45)
}
info := nip11.RelayInformationDocument{
Name: rl.Name,
Description: rl.Description,
PubKey: rl.PubKey,
Contact: rl.Contact,
Icon: rl.IconURL,
SupportedNIPs: supportedNIPs,
Software: "https://github.com/trailriver/khatru",
Version: "n/a",
}
for _, edit := range rl.EditInformation {
edit(r.Context(), &info)
}
json.NewEncoder(w).Encode(info)
}

View File

@@ -1,77 +0,0 @@
package khatru
import (
"net"
"net/http"
"strconv"
"strings"
"github.com/nbd-wtf/go-nostr"
)
func isOlder(previous, next *nostr.Event) bool {
return previous.CreatedAt < next.CreatedAt ||
(previous.CreatedAt == next.CreatedAt && previous.ID > next.ID)
}
func getServiceBaseURL(r *http.Request) string {
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
proto := r.Header.Get("X-Forwarded-Proto")
if proto == "" {
if host == "localhost" {
proto = "http"
} else if strings.Index(host, ":") != -1 {
// has a port number
proto = "http"
} else if _, err := strconv.Atoi(strings.ReplaceAll(host, ".", "")); err == nil {
// it's a naked IP
proto = "http"
} else {
proto = "https"
}
}
return proto + "://" + host
}
var privateMasks = func() []net.IPNet {
privateCIDRs := []string{
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
}
masks := make([]net.IPNet, len(privateCIDRs))
for i, cidr := range privateCIDRs {
_, netw, err := net.ParseCIDR(cidr)
if err != nil {
return nil
}
masks[i] = *netw
}
return masks
}()
func isPrivate(ip net.IP) bool {
for _, mask := range privateMasks {
if mask.Contains(ip) {
return true
}
}
return false
}
func GetIPFromRequest(r *http.Request) string {
if xffh := r.Header.Get("X-Forwarded-For"); xffh != "" {
for _, v := range strings.Split(xffh, ",") {
if ip := net.ParseIP(strings.TrimSpace(v)); ip != nil && ip.IsGlobalUnicast() && !isPrivate(ip) {
return ip.String()
}
}
}
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return ip
}

View File

@@ -1,19 +1,15 @@
package khatru
import (
"context"
"fmt"
"github.com/nbd-wtf/go-nostr"
"github.com/puzpuzpuz/xsync/v3"
"github.com/puzpuzpuz/xsync/v2"
)
type Listener struct {
filters nostr.Filters
cancel context.CancelCauseFunc
}
var listeners = xsync.NewMapOf[*WebSocket, *xsync.MapOf[string, *Listener]]()
var listeners = xsync.NewTypedMapOf[*WebSocket, *xsync.MapOf[string, *Listener]](pointerHasher[WebSocket])
func GetListeningFilters() nostr.Filters {
respfilters := make(nostr.Filters, 0, listeners.Size()*2)
@@ -47,28 +43,24 @@ func GetListeningFilters() nostr.Filters {
return respfilters
}
func setListener(id string, ws *WebSocket, filters nostr.Filters, cancel context.CancelCauseFunc) {
func setListener(id string, ws *WebSocket, filters nostr.Filters) {
subs, _ := listeners.LoadOrCompute(ws, func() *xsync.MapOf[string, *Listener] {
return xsync.NewMapOf[string, *Listener]()
return xsync.NewMapOf[*Listener]()
})
subs.Store(id, &Listener{filters: filters, cancel: cancel})
subs.Store(id, &Listener{filters: filters})
}
// 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
func removeListenerId(ws *WebSocket, id string) {
if subs, ok := listeners.Load(ws); ok {
if listener, ok := subs.LoadAndDelete(id); ok {
listener.cancel(fmt.Errorf("subscription closed by client"))
}
subs.Delete(id)
if subs.Size() == 0 {
listeners.Delete(ws)
}
}
}
// 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
func removeListener(ws *WebSocket) {
listeners.Delete(ws)
}

View File

@@ -1,25 +0,0 @@
package khatru
import (
"encoding/json"
"net/http"
)
func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/nostr+json")
info := *rl.Info
if len(rl.DeleteEvent) > 0 {
info.SupportedNIPs = append(info.SupportedNIPs, 9)
}
if len(rl.CountEvents) > 0 {
info.SupportedNIPs = append(info.SupportedNIPs, 45)
}
for _, ovw := range rl.OverwriteRelayInformation {
info = ovw(r.Context(), r, info)
}
json.NewEncoder(w).Encode(info)
}

267
nip86.go
View File

@@ -1,267 +0,0 @@
package khatru
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"reflect"
"strings"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip86"
)
type RelayManagementAPI struct {
RejectAPICall []func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string)
BanPubKey func(ctx context.Context, pubkey string, reason string) error
ListBannedPubKeys func(ctx context.Context) ([]nip86.PubKeyReason, error)
AllowPubKey func(ctx context.Context, pubkey string, reason string) error
ListAllowedPubKeys func(ctx context.Context) ([]nip86.PubKeyReason, error)
ListEventsNeedingModeration func(ctx context.Context) ([]nip86.IDReason, error)
AllowEvent func(ctx context.Context, id string, reason string) error
BanEvent func(ctx context.Context, id string, reason string) error
ListBannedEvents func(ctx context.Context) ([]nip86.IDReason, error)
ChangeRelayName func(ctx context.Context, name string) error
ChangeRelayDescription func(ctx context.Context, desc string) error
ChangeRelayIcon func(ctx context.Context, icon string) error
AllowKind func(ctx context.Context, kind int) error
DisallowKind func(ctx context.Context, kind int) error
ListAllowedKinds func(ctx context.Context) ([]int, error)
BlockIP func(ctx context.Context, ip net.IP, reason string) error
UnblockIP func(ctx context.Context, ip net.IP, reason string) error
ListBlockedIPs func(ctx context.Context) ([]nip86.IPReason, error)
}
func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/nostr+json+rpc")
var (
resp nip86.Response
ctx = r.Context()
req nip86.Request
mp nip86.MethodParams
evt nostr.Event
payloadHash [32]byte
)
payload, err := io.ReadAll(r.Body)
if err != nil {
resp.Error = "empty request"
goto respond
}
payloadHash = sha256.Sum256(payload)
{
auth := r.Header.Get("Authorization")
spl := strings.Split(auth, "Nostr ")
if len(spl) != 2 {
resp.Error = "missing auth"
goto respond
}
if evtj, err := base64.StdEncoding.DecodeString(spl[1]); err != nil {
resp.Error = "invalid base64 auth"
goto respond
} else if err := json.Unmarshal(evtj, &evt); err != nil {
resp.Error = "invalid auth event json"
goto respond
} else if ok, _ := evt.CheckSignature(); !ok {
resp.Error = "invalid auth event"
goto respond
} else if pht := evt.Tags.GetFirst([]string{"payload", hex.EncodeToString(payloadHash[:])}); pht == nil {
resp.Error = "invalid auth event payload hash"
goto respond
} else if evt.CreatedAt < nostr.Now()-30 {
resp.Error = "auth event is too old"
goto respond
}
}
if err := json.Unmarshal(payload, &req); err != nil {
resp.Error = "invalid json body"
goto respond
}
mp, err = nip86.DecodeRequest(req)
if err != nil {
resp.Error = fmt.Sprintf("invalid params: %s", err)
goto respond
}
ctx = context.WithValue(ctx, nip86HeaderAuthKey, evt.PubKey)
for _, rac := range rl.ManagementAPI.RejectAPICall {
if reject, msg := rac(ctx, mp); reject {
resp.Error = msg
goto respond
}
}
if _, ok := mp.(nip86.SupportedMethods); ok {
mat := reflect.TypeOf(rl.ManagementAPI)
mav := reflect.ValueOf(rl.ManagementAPI)
methods := make([]string, 0, mat.NumField())
for i := 0; i < mat.NumField(); i++ {
field := mat.Field(i)
// danger: this assumes the struct fields are appropriately named
methodName := strings.ToLower(field.Name)
// assign this only if the function was defined
if mav.Field(i).Interface() != nil {
methods[i] = methodName
}
}
resp.Result = methods
} else {
switch thing := mp.(type) {
case nip86.BanPubKey:
if rl.ManagementAPI.BanPubKey == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.BanPubKey(ctx, thing.PubKey, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.ListBannedPubKeys:
if rl.ManagementAPI.ListBannedPubKeys == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if result, err := rl.ManagementAPI.ListBannedPubKeys(ctx); err != nil {
resp.Error = err.Error()
} else {
resp.Result = result
}
case nip86.AllowPubKey:
if rl.ManagementAPI.AllowPubKey == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.AllowPubKey(ctx, thing.PubKey, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.ListAllowedPubKeys:
if rl.ManagementAPI.ListAllowedPubKeys == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if result, err := rl.ManagementAPI.ListAllowedPubKeys(ctx); err != nil {
resp.Error = err.Error()
} else {
resp.Result = result
}
case nip86.BanEvent:
if rl.ManagementAPI.BanEvent == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.BanEvent(ctx, thing.ID, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.AllowEvent:
if rl.ManagementAPI.AllowEvent == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.AllowEvent(ctx, thing.ID, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.ListEventsNeedingModeration:
if rl.ManagementAPI.ListEventsNeedingModeration == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if result, err := rl.ManagementAPI.ListEventsNeedingModeration(ctx); err != nil {
resp.Error = err.Error()
} else {
resp.Result = result
}
case nip86.ListBannedEvents:
if rl.ManagementAPI.ListBannedEvents == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if result, err := rl.ManagementAPI.ListEventsNeedingModeration(ctx); err != nil {
resp.Error = err.Error()
} else {
resp.Result = result
}
case nip86.ChangeRelayName:
if rl.ManagementAPI.ChangeRelayName == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.ChangeRelayName(ctx, thing.Name); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.ChangeRelayDescription:
if rl.ManagementAPI.ChangeRelayDescription == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.ChangeRelayDescription(ctx, thing.Description); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.ChangeRelayIcon:
if rl.ManagementAPI.ChangeRelayIcon == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.ChangeRelayIcon(ctx, thing.IconURL); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.AllowKind:
if rl.ManagementAPI.AllowKind == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.AllowKind(ctx, thing.Kind); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.DisallowKind:
if rl.ManagementAPI.DisallowKind == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.DisallowKind(ctx, thing.Kind); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.ListAllowedKinds:
if rl.ManagementAPI.ListAllowedKinds == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if result, err := rl.ManagementAPI.ListAllowedKinds(ctx); err != nil {
resp.Error = err.Error()
} else {
resp.Result = result
}
case nip86.BlockIP:
if rl.ManagementAPI.BlockIP == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.BlockIP(ctx, thing.IP, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.UnblockIP:
if rl.ManagementAPI.UnblockIP == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.UnblockIP(ctx, thing.IP, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.ListBlockedIPs:
if rl.ManagementAPI.ListBlockedIPs == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if result, err := rl.ManagementAPI.ListBlockedIPs(ctx); err != nil {
resp.Error = err.Error()
} else {
resp.Result = result
}
default:
resp.Error = fmt.Sprintf("method '%s' not known", mp.MethodName())
}
}
respond:
json.NewEncoder(w).Encode(resp)
}

58
plugins/events.go Normal file
View File

@@ -0,0 +1,58 @@
package plugins
import (
"context"
"github.com/nbd-wtf/go-nostr"
)
// 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.
func PreventTooManyIndexableTags(max int) func(context.Context, *nostr.Event) (bool, string) {
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
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, ""
}
}
// 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
allowed := make(map[uint16]struct{}, len(kinds))
for _, kind := range kinds {
allowed[kind] = struct{}{}
if int(kind) > max {
max = int(kind)
}
if int(kind) < min {
min = int(kind)
}
}
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"
}
if event.Kind < min {
return true, "event kind not allowed"
}
// hopefully this map of uint16s is very fast
if _, allowed := allowed[uint16(event.Kind)]; allowed {
return false, ""
}
return true, "event kind not allowed"
}
}

33
plugins/filters.go Normal file
View File

@@ -0,0 +1,33 @@
package plugins
import (
"context"
"fmt"
"github.com/nbd-wtf/go-nostr"
)
func NoPrefixFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
for _, id := range filter.IDs {
if len(id) != 64 {
return true, fmt.Sprintf("filters can only contain full ids")
}
}
for _, pk := range filter.Authors {
if len(pk) != 64 {
return true, fmt.Sprintf("filters can only contain full pubkeys")
}
}
return false, ""
}
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, ""
}

View File

@@ -1,16 +1,14 @@
package policies
package plugins
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.
func RejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, string) {
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) {
@@ -21,13 +19,13 @@ func RejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, strin
senders := filter.Authors
receivers, _ := filter.Tags["p"]
switch {
case ws.AuthedPublicKey == "":
case ws.Authed == "":
// 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):
case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.Authed):
// 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):
case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.Authed):
// allowed filter: ws.authed is sole receiver (filter specifies one or all senders)
return false, ""
default:

View File

@@ -1,102 +0,0 @@
package policies
import (
"context"
"fmt"
"slices"
"strings"
"github.com/nbd-wtf/go-nostr"
)
// 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(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 _, allowed := slices.BinarySearch(kinds, uint16(event.Kind)); allowed {
return false, ""
}
return true, fmt.Sprintf("received event kind %d not allowed", event.Kind)
}
}
func PreventTimestampsInThePast(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, string) {
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(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, 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"
}
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"
}

View File

@@ -1,84 +0,0 @@
package policies
import (
"context"
"slices"
"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, ""
}
// 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
}
}
}
}

View File

@@ -1,43 +0,0 @@
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)
negativeBuckets.Range(func(key K, bucket *atomic.Int32) bool {
newv := bucket.Add(int32(-tokensPerInterval))
if newv <= 0 {
negativeBuckets.Delete(key)
}
return true
})
}
}()
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
}
}

View File

@@ -1,29 +0,0 @@
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, ""
}

View File

@@ -1,42 +0,0 @@
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) {
return rl(khatru.GetIP(ctx)), "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"
}
}

View File

@@ -1,24 +0,0 @@
package policies
import (
"time"
"github.com/fiatjaf/khatru"
)
func ApplySaneDefaults(relay *khatru.Relay) {
relay.RejectEvent = append(relay.RejectEvent,
RejectEventsWithBase64Media,
EventIPRateLimiter(2, time.Minute*3, 5),
)
relay.RejectFilter = append(relay.RejectFilter,
NoEmptyFilters,
NoComplexFilters,
FilterIPRateLimiter(20, time.Minute, 100),
)
relay.RejectConnection = append(relay.RejectConnection,
ConnectionRateLimiter(1, time.Minute*5, 3),
)
}

View File

@@ -10,26 +10,20 @@ import (
"github.com/fasthttp/websocket"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip11"
"github.com/puzpuzpuz/xsync/v3"
"github.com/puzpuzpuz/xsync/v2"
)
func NewRelay() *Relay {
return &Relay{
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
Info: &nip11.RelayInformationDocument{
Software: "https://github.com/fiatjaf/khatru",
Version: "n/a",
SupportedNIPs: []int{1, 11, 42, 70, 86},
},
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
},
clients: xsync.NewMapOf[*websocket.Conn, struct{}](),
clients: xsync.NewTypedMapOf[*websocket.Conn, struct{}](pointerHasher[websocket.Conn]),
serveMux: &http.ServeMux{},
WriteWait: 10 * time.Second,
@@ -40,31 +34,26 @@ func NewRelay() *Relay {
}
type Relay struct {
ServiceURL string
Name string
Description string
PubKey string
Contact string
ServiceURL string // required for nip-42
IconURL string
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
RejectFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
RejectCountFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
RejectConnection []func(r *http.Request) bool
OverwriteDeletionOutcome []func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
OverwriteResponseEvent []func(ctx context.Context, event *nostr.Event)
OverwriteFilter []func(ctx context.Context, filter *nostr.Filter)
OverwriteCountFilter []func(ctx context.Context, filter *nostr.Filter)
OverwriteRelayInformation []func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
StoreEvent []func(ctx context.Context, event *nostr.Event) error
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
OnConnect []func(ctx context.Context)
OnDisconnect []func(ctx context.Context)
OnEventSaved []func(ctx context.Context, event *nostr.Event)
OnEphemeralEvent []func(ctx context.Context, event *nostr.Event)
// setting up handlers here will enable these methods
ManagementAPI RelayManagementAPI
// editing info will affect the NIP-11 responses
Info *nip11.RelayInformationDocument
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg string)
RejectFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
RejectCountFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
OverwriteDeletionOutcome []func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
OverwriteResponseEvent []func(ctx context.Context, event *nostr.Event)
StoreEvent []func(ctx context.Context, event *nostr.Event) error
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
EditInformation []func(ctx context.Context, info *nip11.RelayInformationDocument)
OnAuth []func(ctx context.Context, pubkey string)
OnConnect []func(ctx context.Context)
OnEventSaved []func(ctx context.Context, event *nostr.Event)
// Default logger, as set by NewServer, is a stdlib logger prefixed with "[khatru-relay] ",
// outputting to stderr.
@@ -87,3 +76,8 @@ type Relay struct {
PingPeriod time.Duration // Send pings to peer with this period. Must be less than pongWait.
MaxMessageSize int64 // Maximum message size allowed from peer.
}
func (rl *Relay) RequestAuth(ctx context.Context) {
ws := GetConnection(ctx)
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
}

View File

@@ -1,88 +0,0 @@
package khatru
import (
"context"
"errors"
"sync"
"github.com/nbd-wtf/go-nostr"
)
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) error {
defer eose.Done()
// overwrite the filter (for example, to eliminate some kinds or
// that we know we don't support)
for _, ovw := range rl.OverwriteFilter {
ovw(ctx, &filter)
}
if filter.LimitZero {
// don't do any queries, just subscribe to future events
return nil
}
// then check if we'll reject this filter (we apply this after overwriting
// because we may, for example, remove some things from the incoming filters
// that we know we don't support, and then if the end result is an empty
// filter we can just reject it)
for _, reject := range rl.RejectFilter {
if reject, msg := reject(ctx, filter); reject {
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
}
}
// run the functions to query events (generally just one,
// but we might be fetching stuff from multiple places)
eose.Add(len(rl.QueryEvents))
for _, query := range rl.QueryEvents {
ch, err := query(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
eose.Done()
continue
} else if ch == nil {
eose.Done()
continue
}
go func(ch chan *nostr.Event) {
for event := range ch {
for _, ovw := range rl.OverwriteResponseEvent {
ovw(ctx, event)
}
ws.WriteJSON(nostr.EventEnvelope{SubscriptionID: &id, Event: *event})
}
eose.Done()
}(ch)
}
return nil
}
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
// overwrite the filter (for example, to eliminate some kinds or tags that we know we don't support)
for _, ovw := range rl.OverwriteCountFilter {
ovw(ctx, &filter)
}
// then check if we'll reject this filter
for _, reject := range rl.RejectCountFilter {
if rejecting, msg := reject(ctx, filter); rejecting {
ws.WriteJSON(nostr.NoticeEnvelope(msg))
return 0
}
}
// run the functions to count (generally it will be just one)
var subtotal int64 = 0
for _, count := range rl.CountEvents {
res, err := count(ctx, filter)
if err != nil {
ws.WriteJSON(nostr.NoticeEnvelope(err.Error()))
}
subtotal += res
}
return subtotal
}

93
start_test.go Normal file
View File

@@ -0,0 +1,93 @@
package khatru
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/gobwas/ws/wsutil"
"github.com/nbd-wtf/go-nostr"
)
func TestServerStartShutdown(t *testing.T) {
var (
inited bool
storeInited bool
shutdown bool
)
rl := &testRelay{
name: "test server start",
init: func() error {
inited = true
return nil
},
onShutdown: func(context.Context) { shutdown = true },
storage: &testStorage{
init: func() error { storeInited = true; return nil },
},
}
srv, _ := NewServer(rl)
ready := make(chan bool)
done := make(chan error)
go func() { done <- srv.Start("127.0.0.1", 0, ready); close(done) }()
<-ready
// verify everything's initialized
if !inited {
t.Error("didn't call testRelay.init")
}
if !storeInited {
t.Error("didn't call testStorage.init")
}
// check that http requests are served
if _, err := http.Get("http://" + srv.Addr); err != nil {
t.Errorf("GET %s: %v", srv.Addr, err)
}
// verify server shuts down
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
srv.Shutdown(ctx)
if !shutdown {
t.Error("didn't call testRelay.onShutdown")
}
select {
case err := <-done:
if err != nil {
t.Errorf("srv.Start: %v", err)
}
case <-time.After(time.Second):
t.Error("srv.Start too long to return")
}
}
func TestServerShutdownWebsocket(t *testing.T) {
// set up a new relay server
srv := startTestRelay(t, &testRelay{storage: &testStorage{}})
// connect a client to it
ctx1, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
client, err := nostr.RelayConnect(ctx1, "ws://"+srv.Addr)
if err != nil {
t.Fatalf("nostr.RelayConnectContext: %v", err)
}
// now, shut down the server
ctx2, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
srv.Shutdown(ctx2)
// wait for the client to receive a "connection close"
time.Sleep(1 * time.Second)
err = client.ConnectionError
if e := errors.Unwrap(err); e != nil {
err = e
}
if _, ok := err.(wsutil.ClosedError); !ok {
t.Errorf("client.ConnextionError: %v (%T); want wsutil.ClosedError", err, err)
}
}

91
util_test.go Normal file
View File

@@ -0,0 +1,91 @@
package khatru
import (
"context"
"testing"
"github.com/nbd-wtf/go-nostr"
)
func startTestRelay(t *testing.T, tr *testRelay) *Server {
t.Helper()
srv, _ := NewServer(tr)
started := make(chan bool)
go srv.Start("127.0.0.1", 0, started)
<-started
return srv
}
type testRelay struct {
name string
storage Storage
init func() error
onShutdown func(context.Context)
acceptEvent func(*nostr.Event) bool
}
func (tr *testRelay) Name() string { return tr.name }
func (tr *testRelay) Storage(context.Context) Storage { return tr.storage }
func (tr *testRelay) Init() error {
if fn := tr.init; fn != nil {
return fn()
}
return nil
}
func (tr *testRelay) OnShutdown(ctx context.Context) {
if fn := tr.onShutdown; fn != nil {
fn(ctx)
}
}
func (tr *testRelay) AcceptEvent(ctx context.Context, e *nostr.Event) bool {
if fn := tr.acceptEvent; fn != nil {
return fn(e)
}
return true
}
type testStorage struct {
init func() error
queryEvents func(context.Context, *nostr.Filter) (chan *nostr.Event, error)
deleteEvent func(ctx context.Context, id string, pubkey string) error
saveEvent func(context.Context, *nostr.Event) error
countEvents func(context.Context, *nostr.Filter) (int64, error)
}
func (st *testStorage) Init() error {
if fn := st.init; fn != nil {
return fn()
}
return nil
}
func (st *testStorage) QueryEvents(ctx context.Context, f *nostr.Filter) (chan *nostr.Event, error) {
if fn := st.queryEvents; fn != nil {
return fn(ctx, f)
}
return nil, nil
}
func (st *testStorage) DeleteEvent(ctx context.Context, id string, pubkey string) error {
if fn := st.deleteEvent; fn != nil {
return fn(ctx, id, pubkey)
}
return nil
}
func (st *testStorage) SaveEvent(ctx context.Context, e *nostr.Event) error {
if fn := st.saveEvent; fn != nil {
return fn(ctx, e)
}
return nil
}
func (st *testStorage) CountEvents(ctx context.Context, f *nostr.Filter) (int64, error) {
if fn := st.countEvents; fn != nil {
return fn(ctx, f)
}
return 0, nil
}

View File

@@ -2,60 +2,28 @@ package khatru
import (
"context"
"github.com/nbd-wtf/go-nostr"
"hash/maphash"
"regexp"
"unsafe"
)
const (
wsKey = iota
subscriptionIdKey
nip86HeaderAuthKey
AUTH_CONTEXT_KEY = iota
WS_KEY = iota
)
func RequestAuth(ctx context.Context) {
ws := GetConnection(ctx)
ws.authLock.Lock()
if ws.Authed == nil {
ws.Authed = make(chan struct{})
}
ws.authLock.Unlock()
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
}
var nip20prefixmatcher = regexp.MustCompile(`^\w+: `)
func GetConnection(ctx context.Context) *WebSocket {
wsi := ctx.Value(wsKey)
if wsi != nil {
return wsi.(*WebSocket)
}
return nil
return ctx.Value(WS_KEY).(*WebSocket)
}
func GetAuthed(ctx context.Context) string {
if conn := GetConnection(ctx); conn != nil {
return conn.AuthedPublicKey
authedPubkey := ctx.Value(AUTH_CONTEXT_KEY)
if authedPubkey == nil {
return ""
}
if nip86Auth := ctx.Value(nip86HeaderAuthKey); nip86Auth != nil {
return nip86Auth.(string)
}
return ""
return authedPubkey.(string)
}
func GetIP(ctx context.Context) string {
return GetIPFromRequest(GetConnection(ctx).Request)
}
func GetSubscriptionID(ctx context.Context) string {
return ctx.Value(subscriptionIdKey).(string)
}
func GetOpenSubscriptions(ctx context.Context) []nostr.Filter {
if subs, ok := listeners.Load(GetConnection(ctx)); ok {
res := make([]nostr.Filter, 0, listeners.Size()*2)
subs.Range(func(_ string, sub *Listener) bool {
res = append(res, sub.filters...)
return true
})
return res
}
return nil
}
func pointerHasher[V any](_ maphash.Seed, k *V) uint64 { return uint64(uintptr(unsafe.Pointer(k))) }

View File

@@ -1,7 +1,6 @@
package khatru
import (
"net/http"
"sync"
"github.com/fasthttp/websocket"
@@ -11,15 +10,10 @@ type WebSocket struct {
conn *websocket.Conn
mutex sync.Mutex
// original request
Request *http.Request
// nip42
Challenge string
AuthedPublicKey string
Authed chan struct{}
authLock sync.Mutex
Challenge string
Authed string
WaitingForAuth chan struct{}
}
func (ws *WebSocket) WriteJSON(any any) error {