mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-18 19:36:54 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8df7c9d773 |
19
README.md
19
README.md
@@ -69,11 +69,6 @@ func main() {
|
|||||||
|
|
||||||
// there are many other configurable things you can set
|
// there are many other configurable things you can set
|
||||||
relay.RejectEvent = append(relay.RejectEvent,
|
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) {
|
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||||
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
||||||
return true, "we don't allow this person to write here"
|
return true, "we don't allow this person to write here"
|
||||||
@@ -84,10 +79,6 @@ func main() {
|
|||||||
|
|
||||||
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
||||||
relay.RejectFilter = append(relay.RejectFilter,
|
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) {
|
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
|
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
|
||||||
log.Printf("request from %s\n", pubkey)
|
log.Printf("request from %s\n", pubkey)
|
||||||
@@ -128,13 +119,3 @@ Fear no more. Using the https://github.com/fiatjaf/eventstore module you get a b
|
|||||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
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.
|
|
||||||
|
|||||||
60
adding.go
60
adding.go
@@ -10,17 +10,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AddEvent sends an event through then normal add pipeline, as if it was received from a websocket.
|
// 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) {
|
func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) error {
|
||||||
if evt == nil {
|
if evt == nil {
|
||||||
return false, errors.New("error: event is nil")
|
return errors.New("error: event is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, reject := range rl.RejectEvent {
|
for _, reject := range rl.RejectEvent {
|
||||||
if reject, msg := reject(ctx, evt); reject {
|
if reject, msg := reject(ctx, evt); reject {
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
return false, errors.New("blocked: no reason")
|
return errors.New("blocked: no reason")
|
||||||
} else {
|
} else {
|
||||||
return false, errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,53 +31,29 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast
|
|||||||
oee(ctx, evt)
|
oee(ctx, evt)
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
if evt.Kind == 0 || evt.Kind == 3 || (10000 <= evt.Kind && evt.Kind < 20000) {
|
||||||
// replaceable event, delete before storing
|
// replaceable event, delete before storing
|
||||||
filter := nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}}
|
|
||||||
for _, query := range rl.QueryEvents {
|
for _, query := range rl.QueryEvents {
|
||||||
ch, err := query(ctx, filter)
|
ch, err := query(ctx, nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for previous := range ch {
|
if previous := <-ch; previous != nil && isOlder(previous, evt) {
|
||||||
if isOlder(previous, evt) {
|
for _, del := range rl.DeleteEvent {
|
||||||
for _, del := range rl.DeleteEvent {
|
del(ctx, previous)
|
||||||
del(ctx, previous)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if 30000 <= evt.Kind && evt.Kind < 40000 {
|
} else if 30000 <= evt.Kind && evt.Kind < 40000 {
|
||||||
// parameterized replaceable event, delete before storing
|
// parameterized replaceable event, delete before storing
|
||||||
d := evt.Tags.GetFirst([]string{"d", ""})
|
d := evt.Tags.GetFirst([]string{"d", ""})
|
||||||
if d == nil {
|
if d != nil {
|
||||||
return false, fmt.Errorf("invalid: missing 'd' tag on parameterized replaceable event")
|
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 {
|
||||||
filter := nostr.Filter{Authors: []string{evt.PubKey}, Kinds: []int{evt.Kind}, Tags: nostr.TagMap{"d": []string{(*d)[1]}}}
|
continue
|
||||||
for _, query := range rl.QueryEvents {
|
}
|
||||||
ch, err := query(ctx, filter)
|
if previous := <-ch; previous != nil && isOlder(previous, evt) {
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for previous := range ch {
|
|
||||||
if isOlder(previous, evt) {
|
|
||||||
for _, del := range rl.DeleteEvent {
|
for _, del := range rl.DeleteEvent {
|
||||||
del(ctx, previous)
|
del(ctx, previous)
|
||||||
}
|
}
|
||||||
@@ -91,9 +67,9 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast
|
|||||||
if saveErr := store(ctx, evt); saveErr != nil {
|
if saveErr := store(ctx, evt); saveErr != nil {
|
||||||
switch saveErr {
|
switch saveErr {
|
||||||
case eventstore.ErrDupEvent:
|
case eventstore.ErrDupEvent:
|
||||||
return true, nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf(nostr.NormalizeOKMessage(saveErr.Error(), "error"))
|
return fmt.Errorf(nostr.NormalizeOKMessage(saveErr.Error(), "error"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,5 +79,5 @@ func (rl *Relay) AddEvent(ctx context.Context, evt *nostr.Event) (skipBroadcast
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) erro
|
|||||||
if acceptDeletion {
|
if acceptDeletion {
|
||||||
// delete it
|
// delete it
|
||||||
for _, del := range rl.DeleteEvent {
|
for _, del := range rl.DeleteEvent {
|
||||||
if err := del(ctx, target); err != nil {
|
del(ctx, target)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// fail and stop here
|
// fail and stop here
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fiatjaf/khatru"
|
"github.com/fiatjaf/khatru"
|
||||||
"github.com/fiatjaf/khatru/policies"
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,11 +53,6 @@ func main() {
|
|||||||
|
|
||||||
// there are many other configurable things you can set
|
// there are many other configurable things you can set
|
||||||
relay.RejectEvent = append(relay.RejectEvent,
|
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) {
|
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||||
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
if event.PubKey == "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" {
|
||||||
return true, "we don't allow this person to write here"
|
return true, "we don't allow this person to write here"
|
||||||
@@ -69,10 +63,6 @@ func main() {
|
|||||||
|
|
||||||
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
||||||
relay.RejectFilter = append(relay.RejectFilter,
|
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) {
|
func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
|
if pubkey := khatru.GetAuthed(ctx); pubkey != "" {
|
||||||
log.Printf("request from %s\n", pubkey)
|
log.Printf("request from %s\n", pubkey)
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -5,9 +5,10 @@ go 1.21.4
|
|||||||
require (
|
require (
|
||||||
github.com/fasthttp/websocket v1.5.7
|
github.com/fasthttp/websocket v1.5.7
|
||||||
github.com/fiatjaf/eventstore v0.3.8
|
github.com/fiatjaf/eventstore v0.3.8
|
||||||
github.com/nbd-wtf/go-nostr v0.30.0
|
github.com/nbd-wtf/go-nostr v0.28.1
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
||||||
github.com/rs/cors v1.7.0
|
github.com/rs/cors v1.7.0
|
||||||
|
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -113,8 +113,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
|
|||||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
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 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/nbd-wtf/go-nostr v0.30.0 h1:rN085pe4IxmSBVht8LChZbWLggonjA8hPIk8l4/+Hjk=
|
github.com/nbd-wtf/go-nostr v0.28.1 h1:XQi/lBsigBXHRm7IDBJE7SR9citCh9srgf8sA5iVW3A=
|
||||||
github.com/nbd-wtf/go-nostr v0.30.0/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk=
|
github.com/nbd-wtf/go-nostr v0.28.1/go.mod h1:OQ8sNLFJnsj17BdqZiLSmjJBIFTfDqckEYC3utS4qoY=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -126,6 +126,8 @@ 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/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 h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||||
|
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+uQBDeAhHhwdzB5fSlVdhGcM=
|
||||||
|
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|||||||
15
handlers.go
15
handlers.go
@@ -34,13 +34,6 @@ func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
for _, reject := range rl.RejectConnection {
|
|
||||||
if reject(r) {
|
|
||||||
w.WriteHeader(418) // I'm a teapot
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := rl.upgrader.Upgrade(w, r, nil)
|
conn, err := rl.upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rl.Log.Printf("failed to upgrade websocket: %v\n", err)
|
rl.Log.Printf("failed to upgrade websocket: %v\n", err)
|
||||||
@@ -168,13 +161,12 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
var writeErr error
|
var writeErr error
|
||||||
var skipBroadcast bool
|
|
||||||
if env.Event.Kind == 5 {
|
if env.Event.Kind == 5 {
|
||||||
// this always returns "blocked: " whenever it returns an error
|
// this always returns "blocked: " whenever it returns an error
|
||||||
writeErr = rl.handleDeleteRequest(ctx, &env.Event)
|
writeErr = rl.handleDeleteRequest(ctx, &env.Event)
|
||||||
} else {
|
} else {
|
||||||
// this will also always return a prefixed reason
|
// this will also always return a prefixed reason
|
||||||
skipBroadcast, writeErr = rl.AddEvent(ctx, &env.Event)
|
writeErr = rl.AddEvent(ctx, &env.Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason string
|
var reason string
|
||||||
@@ -183,9 +175,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
for _, ovw := range rl.OverwriteResponseEvent {
|
for _, ovw := range rl.OverwriteResponseEvent {
|
||||||
ovw(ctx, &env.Event)
|
ovw(ctx, &env.Event)
|
||||||
}
|
}
|
||||||
if !skipBroadcast {
|
notifyListeners(&env.Event)
|
||||||
notifyListeners(&env.Event)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
reason = writeErr.Error()
|
reason = writeErr.Error()
|
||||||
if strings.HasPrefix(reason, "auth-required:") {
|
if strings.HasPrefix(reason, "auth-required:") {
|
||||||
@@ -282,6 +272,7 @@ func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/nostr+json")
|
w.Header().Set("Content-Type", "application/nostr+json")
|
||||||
|
|
||||||
info := *rl.Info
|
info := *rl.Info
|
||||||
|
|
||||||
for _, ovw := range rl.OverwriteRelayInformation {
|
for _, ovw := range rl.OverwriteRelayInformation {
|
||||||
info = ovw(r.Context(), r, info)
|
info = ovw(r.Context(), r, info)
|
||||||
}
|
}
|
||||||
|
|||||||
41
helpers.go
41
helpers.go
@@ -1,7 +1,6 @@
|
|||||||
package khatru
|
package khatru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -35,43 +34,3 @@ func getServiceBaseURL(r *http.Request) string {
|
|||||||
}
|
}
|
||||||
return proto + "://" + host
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package policies
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
)
|
)
|
||||||
@@ -15,9 +14,6 @@ import (
|
|||||||
// If ignoreKinds is given this restriction will not apply to these kinds (useful for allowing a bigger).
|
// 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.
|
// If onlyKinds is given then all other kinds will be ignored.
|
||||||
func PreventTooManyIndexableTags(max int, ignoreKinds []int, onlyKinds []int) func(context.Context, *nostr.Event) (bool, string) {
|
func PreventTooManyIndexableTags(max int, ignoreKinds []int, onlyKinds []int) func(context.Context, *nostr.Event) (bool, string) {
|
||||||
slices.Sort(ignoreKinds)
|
|
||||||
slices.Sort(onlyKinds)
|
|
||||||
|
|
||||||
ignore := func(kind int) bool { return false }
|
ignore := func(kind int) bool { return false }
|
||||||
if len(ignoreKinds) > 0 {
|
if len(ignoreKinds) > 0 {
|
||||||
ignore = func(kind int) bool {
|
ignore = func(kind int) bool {
|
||||||
@@ -78,25 +74,21 @@ func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Even
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort the kinds in increasing order
|
|
||||||
slices.Sort(kinds)
|
|
||||||
|
|
||||||
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
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:
|
// 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
|
// we would have to ensure that the kind number is within the bounds of a uint16 anyway
|
||||||
if event.Kind > max {
|
if event.Kind > max {
|
||||||
return true, fmt.Sprintf("event kind not allowed (it should be lower than %d)", max)
|
return true, "event kind not allowed"
|
||||||
}
|
}
|
||||||
if event.Kind < min {
|
if event.Kind < min {
|
||||||
return true, fmt.Sprintf("event kind not allowed (it should be higher than %d)", min)
|
return true, "event kind not allowed"
|
||||||
}
|
}
|
||||||
|
|
||||||
// hopefully this map of uint16s is very fast
|
// hopefully this map of uint16s is very fast
|
||||||
if _, allowed := slices.BinarySearch(kinds, uint16(event.Kind)); allowed {
|
if _, allowed := slices.BinarySearch(kinds, uint16(event.Kind)); allowed {
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
return true, "event kind not allowed"
|
||||||
return true, fmt.Sprintf("received event kind %d not allowed", event.Kind)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +109,3 @@ func PreventTimestampsInTheFuture(thresholdSeconds nostr.Timestamp) func(context
|
|||||||
return false, ""
|
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"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package policies
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
@@ -47,7 +48,7 @@ func NoSearchQueries(ctx context.Context, filter nostr.Filter) (reject bool, msg
|
|||||||
func RemoveSearchQueries(ctx context.Context, filter *nostr.Filter) {
|
func RemoveSearchQueries(ctx context.Context, filter *nostr.Filter) {
|
||||||
if filter.Search != "" {
|
if filter.Search != "" {
|
||||||
filter.Search = ""
|
filter.Search = ""
|
||||||
filter.LimitZero = true // signals that this query should be just skipped
|
filter.Limit = -1 // signals that this query should be just skipped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ func RemoveAllButKinds(kinds ...uint16) func(context.Context, *nostr.Filter) {
|
|||||||
}
|
}
|
||||||
filter.Kinds = newKinds
|
filter.Kinds = newKinds
|
||||||
if len(filter.Kinds) == 0 {
|
if len(filter.Kinds) == 0 {
|
||||||
filter.LimitZero = true // signals that this query should be just skipped
|
filter.Limit = -1 // signals that this query should be just skipped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,7 @@ func RemoveAllButTags(tagNames ...string) func(context.Context, *nostr.Filter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(filter.Tags) == 0 {
|
if len(filter.Tags) == 0 {
|
||||||
filter.LimitZero = true // signals that this query should be just skipped
|
filter.Limit = -1 // signals that this query should be just skipped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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, ""
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
75
relay.go
75
relay.go
@@ -2,6 +2,7 @@ package khatru
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,39 +14,12 @@ import (
|
|||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
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, 70},
|
|
||||||
},
|
|
||||||
|
|
||||||
upgrader: websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool { return true },
|
|
||||||
},
|
|
||||||
|
|
||||||
clients: xsync.NewMapOf[*websocket.Conn, struct{}](),
|
|
||||||
serveMux: &http.ServeMux{},
|
|
||||||
|
|
||||||
WriteWait: 10 * time.Second,
|
|
||||||
PongWait: 60 * time.Second,
|
|
||||||
PingPeriod: 30 * time.Second,
|
|
||||||
MaxMessageSize: 512000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Relay struct {
|
type Relay struct {
|
||||||
ServiceURL string
|
ServiceURL string
|
||||||
|
|
||||||
RejectEvent []func(ctx context.Context, event *nostr.Event) (reject bool, msg 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)
|
RejectFilter []func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
||||||
RejectCountFilter []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)
|
OverwriteDeletionOutcome []func(ctx context.Context, target *nostr.Event, deletion *nostr.Event) (acceptDeletion bool, msg string)
|
||||||
OverwriteResponseEvent []func(ctx context.Context, event *nostr.Event)
|
OverwriteResponseEvent []func(ctx context.Context, event *nostr.Event)
|
||||||
OverwriteFilter []func(ctx context.Context, filter *nostr.Filter)
|
OverwriteFilter []func(ctx context.Context, filter *nostr.Filter)
|
||||||
@@ -60,7 +34,7 @@ type Relay struct {
|
|||||||
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
||||||
OnEphemeralEvent []func(ctx context.Context, event *nostr.Event)
|
OnEphemeralEvent []func(ctx context.Context, event *nostr.Event)
|
||||||
|
|
||||||
// editing info will affect
|
// editing info will affect the responses to the NIP-11 endpoint
|
||||||
Info *nip11.RelayInformationDocument
|
Info *nip11.RelayInformationDocument
|
||||||
|
|
||||||
// Default logger, as set by NewServer, is a stdlib logger prefixed with "[khatru-relay] ",
|
// Default logger, as set by NewServer, is a stdlib logger prefixed with "[khatru-relay] ",
|
||||||
@@ -83,4 +57,49 @@ type Relay struct {
|
|||||||
PongWait time.Duration // Time allowed to read the next pong message from the peer.
|
PongWait time.Duration // Time allowed to read the next pong message from the peer.
|
||||||
PingPeriod time.Duration // Send pings to peer with this period. Must be less than pongWait.
|
PingPeriod time.Duration // Send pings to peer with this period. Must be less than pongWait.
|
||||||
MaxMessageSize int64 // Maximum message size allowed from peer.
|
MaxMessageSize int64 // Maximum message size allowed from peer.
|
||||||
|
|
||||||
|
// this context is used for all things inside the relay
|
||||||
|
Context context.Context
|
||||||
|
cancel context.CancelCauseFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRelay() *Relay {
|
||||||
|
return NewRelayWithContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRelayWithContext(ctx context.Context) *Relay {
|
||||||
|
ctx, cancel := context.WithCancelCause(ctx)
|
||||||
|
|
||||||
|
rl := &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, 70},
|
||||||
|
},
|
||||||
|
|
||||||
|
upgrader: websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
},
|
||||||
|
|
||||||
|
clients: xsync.NewMapOf[*websocket.Conn, struct{}](),
|
||||||
|
serveMux: &http.ServeMux{},
|
||||||
|
|
||||||
|
WriteWait: 10 * time.Second,
|
||||||
|
PongWait: 60 * time.Second,
|
||||||
|
PingPeriod: 30 * time.Second,
|
||||||
|
MaxMessageSize: 512000,
|
||||||
|
|
||||||
|
Context: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
return rl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *Relay) Close() {
|
||||||
|
rl.cancel(fmt.Errorf("Close called"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
|||||||
ovw(ctx, &filter)
|
ovw(ctx, &filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter.LimitZero {
|
if filter.Limit < 0 {
|
||||||
// don't do any queries, just subscribe to future events
|
// this is a special situation through which the implementor signals to us that it doesn't want
|
||||||
|
// to event perform any queries whatsoever
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
|||||||
// filter we can just reject it)
|
// filter we can just reject it)
|
||||||
for _, reject := range rl.RejectFilter {
|
for _, reject := range rl.RejectFilter {
|
||||||
if reject, msg := reject(ctx, filter); reject {
|
if reject, msg := reject(ctx, filter); reject {
|
||||||
|
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||||
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
return errors.New(nostr.NormalizeOKMessage(msg, "blocked"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
start.go
3
start.go
@@ -2,6 +2,7 @@ package khatru
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -48,6 +49,8 @@ func (rl *Relay) Start(host string, port int, started ...chan bool) error {
|
|||||||
|
|
||||||
// Shutdown sends a websocket close control message to all connected clients.
|
// Shutdown sends a websocket close control message to all connected clients.
|
||||||
func (rl *Relay) Shutdown(ctx context.Context) {
|
func (rl *Relay) Shutdown(ctx context.Context) {
|
||||||
|
rl.cancel(fmt.Errorf("Shutdown called"))
|
||||||
|
|
||||||
rl.httpServer.Shutdown(ctx)
|
rl.httpServer.Shutdown(ctx)
|
||||||
|
|
||||||
rl.clients.Range(func(conn *websocket.Conn, _ struct{}) bool {
|
rl.clients.Range(func(conn *websocket.Conn, _ struct{}) bool {
|
||||||
|
|||||||
3
utils.go
3
utils.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/sebest/xff"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -30,7 +31,7 @@ func GetAuthed(ctx context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetIP(ctx context.Context) string {
|
func GetIP(ctx context.Context) string {
|
||||||
return GetIPFromRequest(GetConnection(ctx).Request)
|
return xff.GetRemoteAddr(GetConnection(ctx).Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSubscriptionID(ctx context.Context) string {
|
func GetSubscriptionID(ctx context.Context) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user