mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-22 04:37:56 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5823515d27 | ||
|
|
9273a4b809 | ||
|
|
ddfc9ab64a | ||
|
|
375236cfe2 | ||
|
|
35e801379a | ||
|
|
22da06b629 | ||
|
|
7bfde76ab1 | ||
|
|
ad92d0b051 | ||
|
|
728417852e | ||
|
|
3c1b062eb8 | ||
|
|
84d01dc1d3 | ||
|
|
888ac8c1c0 | ||
|
|
e1fd6aaa56 | ||
|
|
386a89676a | ||
|
|
90697ad3d3 | ||
|
|
8c8a435a0b | ||
|
|
d608c67791 | ||
|
|
c0069f1e1b | ||
|
|
7a221cf9f0 | ||
|
|
194ec994d7 | ||
|
|
d592bd95a9 | ||
|
|
2edf754907 | ||
|
|
18e4904a00 | ||
|
|
591b49fe73 | ||
|
|
5db3b5fb8b | ||
|
|
dcdf86c4e4 |
@@ -34,7 +34,7 @@ func main() {
|
|||||||
relay.Info.Name = "my relay"
|
relay.Info.Name = "my relay"
|
||||||
relay.Info.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
relay.Info.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||||
relay.Info.Description = "this is my custom relay"
|
relay.Info.Description = "this is my custom relay"
|
||||||
relay.Info.IconURL = "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"
|
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
|
// you must bring your own storage scheme -- if you want to have any
|
||||||
store := make(map[string]*nostr.Event, 120)
|
store := make(map[string]*nostr.Event, 120)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fiatjaf/eventstore/lmdb"
|
"github.com/fiatjaf/eventstore/lmdb"
|
||||||
"github.com/fiatjaf/khatru"
|
"github.com/fiatjaf/khatru"
|
||||||
"github.com/fiatjaf/khatru/plugins"
|
"github.com/fiatjaf/khatru/policies"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ func main() {
|
|||||||
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)
|
||||||
|
|
||||||
relay.RejectEvent = append(relay.RejectEvent, plugins.PreventTooManyIndexableTags(10))
|
relay.RejectEvent = append(relay.RejectEvent, policies.PreventTooManyIndexableTags(10, nil, nil))
|
||||||
relay.RejectFilter = append(relay.RejectFilter, plugins.NoComplexFilters)
|
relay.RejectFilter = append(relay.RejectFilter, policies.NoComplexFilters)
|
||||||
|
|
||||||
relay.OnEventSaved = append(relay.OnEventSaved, func(ctx context.Context, event *nostr.Event) {
|
relay.OnEventSaved = append(relay.OnEventSaved, func(ctx context.Context, event *nostr.Event) {
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -60,15 +60,20 @@ func main() {
|
|||||||
return false, "" // anyone else can
|
return false, "" // anyone else can
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
relay.OnConnect = append(relay.OnConnect,
|
|
||||||
func(ctx context.Context) {
|
// you can request auth by rejecting an event or a request with the prefix "auth-required: "
|
||||||
// request NIP-42 AUTH from everybody
|
relay.RejectFilter = append(relay.RejectFilter,
|
||||||
relay.RequestAuth(ctx)
|
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"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
relay.OnAuth = append(relay.OnAuth,
|
relay.OnAuth = append(relay.OnAuth,
|
||||||
func(ctx context.Context, pubkey string) {
|
func(ctx context.Context, pubkey string) {
|
||||||
// and when they auth we just log that for nothing
|
// and when they auth we can just log that for nothing
|
||||||
log.Println(pubkey + " is authed!")
|
log.Println(pubkey + " is authed!")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.21.0
|
|||||||
require (
|
require (
|
||||||
github.com/fasthttp/websocket v1.5.3
|
github.com/fasthttp/websocket v1.5.3
|
||||||
github.com/fiatjaf/eventstore v0.1.0
|
github.com/fiatjaf/eventstore v0.1.0
|
||||||
github.com/nbd-wtf/go-nostr v0.25.7
|
github.com/nbd-wtf/go-nostr v0.26.0
|
||||||
github.com/puzpuzpuz/xsync/v2 v2.5.1
|
github.com/puzpuzpuz/xsync/v2 v2.5.1
|
||||||
github.com/rs/cors v1.7.0
|
github.com/rs/cors v1.7.0
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
|
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -90,8 +90,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.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
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/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/nbd-wtf/go-nostr v0.25.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM=
|
github.com/nbd-wtf/go-nostr v0.26.0 h1:Tofbs9i8DD5iEKIhLlWFO7kfWpvmUG16fEyW30MzHVQ=
|
||||||
github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
|
github.com/nbd-wtf/go-nostr v0.26.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
|
||||||
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=
|
||||||
|
|||||||
216
handlers.go
216
handlers.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -14,22 +15,25 @@ import (
|
|||||||
"github.com/fasthttp/websocket"
|
"github.com/fasthttp/websocket"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip42"
|
"github.com/nbd-wtf/go-nostr/nip42"
|
||||||
|
"github.com/rs/cors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler interface.
|
// ServeHTTP implements http.Handler interface.
|
||||||
func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if rl.ServiceURL == "" {
|
||||||
|
rl.ServiceURL = getServiceBaseURL(r)
|
||||||
|
}
|
||||||
|
|
||||||
if r.Header.Get("Upgrade") == "websocket" {
|
if r.Header.Get("Upgrade") == "websocket" {
|
||||||
rl.HandleWebsocket(w, r)
|
rl.HandleWebsocket(w, r)
|
||||||
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
||||||
rl.HandleNIP11(w, r)
|
cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP11)).ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
rl.serveMux.ServeHTTP(w, r)
|
rl.serveMux.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -43,21 +47,31 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
rand.Read(challenge)
|
rand.Read(challenge)
|
||||||
|
|
||||||
ws := &WebSocket{
|
ws := &WebSocket{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
Challenge: hex.EncodeToString(challenge),
|
Request: r,
|
||||||
WaitingForAuth: make(chan struct{}),
|
Challenge: hex.EncodeToString(challenge),
|
||||||
|
Authed: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(
|
||||||
|
context.WithValue(
|
||||||
|
context.Background(),
|
||||||
|
WS_KEY, ws,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
kill := func() {
|
||||||
|
ticker.Stop()
|
||||||
|
cancel()
|
||||||
|
if _, ok := rl.clients.Load(conn); ok {
|
||||||
|
conn.Close()
|
||||||
|
rl.clients.Delete(conn)
|
||||||
|
removeListener(ws)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reader
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer kill()
|
||||||
ticker.Stop()
|
|
||||||
if _, ok := rl.clients.Load(conn); ok {
|
|
||||||
conn.Close()
|
|
||||||
rl.clients.Delete(conn)
|
|
||||||
removeListener(ws)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
conn.SetReadLimit(rl.MaxMessageSize)
|
conn.SetReadLimit(rl.MaxMessageSize)
|
||||||
conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
conn.SetReadDeadline(time.Now().Add(rl.PongWait))
|
||||||
@@ -75,6 +89,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if websocket.IsUnexpectedCloseError(
|
if websocket.IsUnexpectedCloseError(
|
||||||
err,
|
err,
|
||||||
|
websocket.CloseNormalClosure, // 1000
|
||||||
websocket.CloseGoingAway, // 1001
|
websocket.CloseGoingAway, // 1001
|
||||||
websocket.CloseNoStatusReceived, // 1005
|
websocket.CloseNoStatusReceived, // 1005
|
||||||
websocket.CloseAbnormalClosure, // 1006
|
websocket.CloseAbnormalClosure, // 1006
|
||||||
@@ -90,152 +105,113 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func(message []byte) {
|
go func(message []byte) {
|
||||||
ctx = context.Background()
|
envelope := nostr.ParseMessage(message)
|
||||||
|
if envelope == nil {
|
||||||
var request []json.RawMessage
|
|
||||||
if err := json.Unmarshal(message, &request); err != nil {
|
|
||||||
// stop silently
|
// stop silently
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(request) < 2 {
|
switch env := envelope.(type) {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope("request has less than 2 parameters"))
|
case *nostr.EventEnvelope:
|
||||||
return
|
// check id
|
||||||
}
|
hash := sha256.Sum256(env.Event.Serialize())
|
||||||
|
id := hex.EncodeToString(hash[:])
|
||||||
var typ string
|
if id != env.Event.ID {
|
||||||
json.Unmarshal(request[0], &typ)
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: id is computed incorrectly"})
|
||||||
|
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check serialization
|
// check signature
|
||||||
serialized := evt.Serialize()
|
if ok, err := env.Event.CheckSignature(); err != nil {
|
||||||
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to verify signature"})
|
||||||
// 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 {
|
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "error: failed to verify signature"})
|
|
||||||
return
|
return
|
||||||
} else if !ok {
|
} else if !ok {
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "invalid: signature is invalid"})
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: signature is invalid"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
if evt.Kind == 5 {
|
if env.Event.Kind == 5 {
|
||||||
err = rl.handleDeleteRequest(ctx, &evt)
|
err = rl.handleDeleteRequest(ctx, &env.Event)
|
||||||
} else {
|
} else {
|
||||||
err = rl.AddEvent(ctx, &evt)
|
err = rl.AddEvent(ctx, &env.Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason string
|
var reason string
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ok = true
|
ok = true
|
||||||
} else {
|
} else {
|
||||||
reason = err.Error()
|
reason = nostr.NormalizeOKMessage(err.Error(), "blocked")
|
||||||
|
if isAuthRequired(reason) {
|
||||||
|
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: ok, Reason: reason})
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: ok, Reason: reason})
|
||||||
case "COUNT":
|
case *nostr.CountEnvelope:
|
||||||
if rl.CountEvents == nil {
|
if rl.CountEvents == nil {
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope("this relay does not support NIP-45"))
|
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: "unsupported: this relay does not support NIP-45"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var id string
|
|
||||||
json.Unmarshal(request[1], &id)
|
|
||||||
if id == "" {
|
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope("COUNT has no <id>"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
filters := make(nostr.Filters, len(request)-2)
|
for _, filter := range env.Filters {
|
||||||
for i, filterReq := range request[2:] {
|
total += rl.handleCountRequest(ctx, ws, filter)
|
||||||
if err := json.Unmarshal(filterReq, &filters[i]); err != nil {
|
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
total += rl.handleCountRequest(ctx, ws, filters[i])
|
|
||||||
}
|
}
|
||||||
|
ws.WriteJSON(nostr.CountEnvelope{SubscriptionID: env.SubscriptionID, Count: &total})
|
||||||
ws.WriteJSON([]interface{}{"COUNT", id, map[string]int64{"count": total}})
|
case *nostr.ReqEnvelope:
|
||||||
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 := sync.WaitGroup{}
|
||||||
eose.Add(len(request[2:]))
|
eose.Add(len(env.Filters))
|
||||||
|
|
||||||
for i, filterReq := range request[2:] {
|
// a context just for the "stored events" request handler
|
||||||
if err := json.Unmarshal(filterReq, &filters[i]); err != nil {
|
reqCtx, cancelReqCtx := context.WithCancelCause(ctx)
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope("failed to decode filter"))
|
|
||||||
eose.Done()
|
// handle each filter separately -- dispatching events as they're loaded from databases
|
||||||
continue
|
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 := nostr.NormalizeOKMessage(err.Error(), "blocked")
|
||||||
|
if isAuthRequired(reason) {
|
||||||
|
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
|
||||||
|
}
|
||||||
|
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: reason})
|
||||||
|
cancelReqCtx(fmt.Errorf("filter rejected"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go rl.handleRequest(ctx, id, &eose, ws, filters[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
// when all events have been loaded from databases and dispatched
|
||||||
|
// we can cancel the context and fire the EOSE message
|
||||||
eose.Wait()
|
eose.Wait()
|
||||||
ws.WriteJSON(nostr.EOSEEnvelope(id))
|
cancelReqCtx(nil)
|
||||||
|
ws.WriteJSON(nostr.EOSEEnvelope(env.SubscriptionID))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
setListener(id, ws, filters)
|
setListener(env.SubscriptionID, ws, env.Filters, cancelReqCtx)
|
||||||
case "CLOSE":
|
case *nostr.CloseEnvelope:
|
||||||
var id string
|
removeListenerId(ws, string(*env))
|
||||||
json.Unmarshal(request[1], &id)
|
case *nostr.AuthEnvelope:
|
||||||
if id == "" {
|
wsBaseUrl := strings.Replace(rl.ServiceURL, "http", "ws", 1)
|
||||||
ws.WriteJSON(nostr.NoticeEnvelope("CLOSE has no <id>"))
|
if pubkey, ok := nip42.ValidateAuthEvent(&env.Event, ws.Challenge, wsBaseUrl); ok {
|
||||||
return
|
ws.AuthedPublicKey = pubkey
|
||||||
}
|
close(ws.Authed)
|
||||||
|
ctx = context.WithValue(ctx, AUTH_CONTEXT_KEY, pubkey)
|
||||||
removeListenerId(ws, id)
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: true})
|
||||||
case "AUTH":
|
} else {
|
||||||
if rl.ServiceURL != "" {
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to authenticate"})
|
||||||
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 {
|
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "error: failed to authenticate"})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(message)
|
}(message)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// writer
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer kill()
|
||||||
ticker.Stop()
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
err := ws.WriteMessage(websocket.PingMessage, nil)
|
err := ws.WriteMessage(websocket.PingMessage, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -251,5 +227,11 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
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")
|
||||||
json.NewEncoder(w).Encode(rl.Info)
|
|
||||||
|
info := *rl.Info
|
||||||
|
for _, ovw := range rl.OverwriteRelayInformation {
|
||||||
|
info = ovw(r.Context(), r, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(info)
|
||||||
}
|
}
|
||||||
|
|||||||
55
helpers.go
Normal file
55
helpers.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package khatru
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hash/maphash"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AUTH_CONTEXT_KEY = iota
|
||||||
|
WS_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
var nip20prefixmatcher = regexp.MustCompile(`^\w+: `)
|
||||||
|
|
||||||
|
func pointerHasher[V any](_ maphash.Seed, k *V) uint64 {
|
||||||
|
return uint64(uintptr(unsafe.Pointer(k)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOlder(previous, next *nostr.Event) bool {
|
||||||
|
return previous.CreatedAt < next.CreatedAt ||
|
||||||
|
(previous.CreatedAt == next.CreatedAt && previous.ID > next.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAuthRequired(msg string) bool {
|
||||||
|
idx := strings.IndexByte(msg, ':')
|
||||||
|
return msg[0:idx] == "auth-required"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
18
listener.go
18
listener.go
@@ -1,12 +1,16 @@
|
|||||||
package khatru
|
package khatru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/puzpuzpuz/xsync/v2"
|
"github.com/puzpuzpuz/xsync/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listener struct {
|
type Listener struct {
|
||||||
filters nostr.Filters
|
filters nostr.Filters
|
||||||
|
cancel context.CancelCauseFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
var listeners = xsync.NewTypedMapOf[*WebSocket, *xsync.MapOf[string, *Listener]](pointerHasher[WebSocket])
|
var listeners = xsync.NewTypedMapOf[*WebSocket, *xsync.MapOf[string, *Listener]](pointerHasher[WebSocket])
|
||||||
@@ -43,24 +47,28 @@ func GetListeningFilters() nostr.Filters {
|
|||||||
return respfilters
|
return respfilters
|
||||||
}
|
}
|
||||||
|
|
||||||
func setListener(id string, ws *WebSocket, filters nostr.Filters) {
|
func setListener(id string, ws *WebSocket, filters nostr.Filters, cancel context.CancelCauseFunc) {
|
||||||
subs, _ := listeners.LoadOrCompute(ws, func() *xsync.MapOf[string, *Listener] {
|
subs, _ := listeners.LoadOrCompute(ws, func() *xsync.MapOf[string, *Listener] {
|
||||||
return xsync.NewMapOf[*Listener]()
|
return xsync.NewMapOf[*Listener]()
|
||||||
})
|
})
|
||||||
subs.Store(id, &Listener{filters: filters})
|
subs.Store(id, &Listener{filters: filters, cancel: cancel})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a specific subscription id from listeners for a given ws client
|
// remove a specific subscription id from listeners for a given ws client
|
||||||
|
// and cancel its specific context
|
||||||
func removeListenerId(ws *WebSocket, id string) {
|
func removeListenerId(ws *WebSocket, id string) {
|
||||||
if subs, ok := listeners.Load(ws); ok {
|
if subs, ok := listeners.Load(ws); ok {
|
||||||
subs.Delete(id)
|
if listener, ok := subs.LoadAndDelete(id); ok {
|
||||||
|
listener.cancel(fmt.Errorf("subscription closed by client"))
|
||||||
|
}
|
||||||
if subs.Size() == 0 {
|
if subs.Size() == 0 {
|
||||||
listeners.Delete(ws)
|
listeners.Delete(ws)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove WebSocket conn from listeners
|
// remove WebSocket conn from listeners
|
||||||
|
// (no need to cancel contexts as they are all inherited from the main connection context)
|
||||||
func removeListener(ws *WebSocket) {
|
func removeListener(ws *WebSocket) {
|
||||||
listeners.Delete(ws)
|
listeners.Delete(ws)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,37 @@
|
|||||||
package plugins
|
package policies
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PreventTooManyIndexableTags returns a function that can be used as a RejectFilter that will reject
|
// 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.
|
// events with more indexable (single-character) tags than the specified number.
|
||||||
func PreventTooManyIndexableTags(max int) func(context.Context, *nostr.Event) (bool, string) {
|
//
|
||||||
|
// 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) {
|
||||||
|
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) {
|
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||||
|
if ignore(event.Kind) {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
ntags := 0
|
ntags := 0
|
||||||
for _, tag := range event.Tags {
|
for _, tag := range event.Tags {
|
||||||
if len(tag) > 0 && len(tag[0]) == 1 {
|
if len(tag) > 0 && len(tag[0]) == 1 {
|
||||||
@@ -42,9 +64,7 @@ func PreventLargeTags(maxTagValueLen int) func(context.Context, *nostr.Event) (b
|
|||||||
func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Event) (bool, string) {
|
func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Event) (bool, string) {
|
||||||
max := 0
|
max := 0
|
||||||
min := 0
|
min := 0
|
||||||
allowed := make(map[uint16]struct{}, len(kinds))
|
|
||||||
for _, kind := range kinds {
|
for _, kind := range kinds {
|
||||||
allowed[kind] = struct{}{}
|
|
||||||
if int(kind) > max {
|
if int(kind) > max {
|
||||||
max = int(kind)
|
max = int(kind)
|
||||||
}
|
}
|
||||||
@@ -64,7 +84,7 @@ func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Even
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hopefully this map of uint16s is very fast
|
// hopefully this map of uint16s is very fast
|
||||||
if _, allowed := allowed[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, "event kind not allowed"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package plugins
|
package policies
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NoComplexFilters disallows filters with more than 2 tags.
|
||||||
func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
items := len(filter.Tags) + len(filter.Kinds)
|
items := len(filter.Tags) + len(filter.Kinds)
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, ms
|
|||||||
return false, ""
|
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) {
|
func NoEmptyFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
c := len(filter.Kinds) + len(filter.IDs) + len(filter.Authors)
|
c := len(filter.Kinds) + len(filter.IDs) + len(filter.Authors)
|
||||||
for _, tagItems := range filter.Tags {
|
for _, tagItems := range filter.Tags {
|
||||||
@@ -28,6 +30,13 @@ func NoEmptyFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg
|
|||||||
return false, ""
|
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) {
|
func NoSearchQueries(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
if filter.Search != "" {
|
if filter.Search != "" {
|
||||||
return true, "search is not supported"
|
return true, "search is not supported"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package plugins
|
package policies
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func rejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, string) {
|
// RejectKind04Snoopers prevents reading NIP-04 messages from people not involved in the conversation.
|
||||||
|
func RejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, string) {
|
||||||
// prevent kind-4 events from being returned to unauthed users,
|
// prevent kind-4 events from being returned to unauthed users,
|
||||||
// only when authentication is a thing
|
// only when authentication is a thing
|
||||||
if !slices.Contains(filter.Kinds, 4) {
|
if !slices.Contains(filter.Kinds, 4) {
|
||||||
@@ -19,13 +20,13 @@ func rejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, strin
|
|||||||
senders := filter.Authors
|
senders := filter.Authors
|
||||||
receivers, _ := filter.Tags["p"]
|
receivers, _ := filter.Tags["p"]
|
||||||
switch {
|
switch {
|
||||||
case ws.Authed == "":
|
case ws.AuthedPublicKey == "":
|
||||||
// not authenticated
|
// not authenticated
|
||||||
return true, "restricted: this relay does not serve kind-4 to unauthenticated users, does your client implement NIP-42?"
|
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.Authed):
|
case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.AuthedPublicKey):
|
||||||
// allowed filter: ws.authed is sole sender (filter specifies one or all receivers)
|
// allowed filter: ws.authed is sole sender (filter specifies one or all receivers)
|
||||||
return false, ""
|
return false, ""
|
||||||
case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.Authed):
|
case len(receivers) == 1 && len(senders) < 2 && (receivers[0] == ws.AuthedPublicKey):
|
||||||
// allowed filter: ws.authed is sole receiver (filter specifies one or all senders)
|
// allowed filter: ws.authed is sole receiver (filter specifies one or all senders)
|
||||||
return false, ""
|
return false, ""
|
||||||
default:
|
default:
|
||||||
41
relay.go
41
relay.go
@@ -18,8 +18,9 @@ func NewRelay() *Relay {
|
|||||||
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
|
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
|
||||||
|
|
||||||
Info: &nip11.RelayInformationDocument{
|
Info: &nip11.RelayInformationDocument{
|
||||||
Software: "https://github.com/fiatjaf/khatru",
|
Software: "https://github.com/fiatjaf/khatru",
|
||||||
Version: "n/a",
|
Version: "n/a",
|
||||||
|
SupportedNIPs: make([]int, 0),
|
||||||
},
|
},
|
||||||
|
|
||||||
upgrader: websocket.Upgrader{
|
upgrader: websocket.Upgrader{
|
||||||
@@ -39,22 +40,23 @@ func NewRelay() *Relay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Relay struct {
|
type Relay struct {
|
||||||
ServiceURL string // required for nip-42
|
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)
|
||||||
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)
|
||||||
OverwriteCountFilter []func(ctx context.Context, filter *nostr.Filter)
|
OverwriteCountFilter []func(ctx context.Context, filter *nostr.Filter)
|
||||||
StoreEvent []func(ctx context.Context, event *nostr.Event) error
|
OverwriteRelayInformation []func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
|
||||||
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
|
StoreEvent []func(ctx context.Context, event *nostr.Event) error
|
||||||
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
|
DeleteEvent []func(ctx context.Context, event *nostr.Event) error
|
||||||
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
|
QueryEvents []func(ctx context.Context, filter nostr.Filter) (chan *nostr.Event, error)
|
||||||
OnAuth []func(ctx context.Context, pubkey string)
|
CountEvents []func(ctx context.Context, filter nostr.Filter) (int64, error)
|
||||||
OnConnect []func(ctx context.Context)
|
OnAuth []func(ctx context.Context, pubkey string)
|
||||||
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
OnConnect []func(ctx context.Context)
|
||||||
|
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
||||||
|
|
||||||
// editing info will affect
|
// editing info will affect
|
||||||
Info *nip11.RelayInformationDocument
|
Info *nip11.RelayInformationDocument
|
||||||
@@ -80,8 +82,3 @@ type Relay struct {
|
|||||||
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.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Relay) RequestAuth(ctx context.Context) {
|
|
||||||
ws := GetConnection(ctx)
|
|
||||||
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package khatru
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) {
|
func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGroup, ws *WebSocket, filter nostr.Filter) error {
|
||||||
defer eose.Done()
|
defer eose.Done()
|
||||||
|
|
||||||
// overwrite the filter (for example, to eliminate some kinds or
|
// overwrite the filter (for example, to eliminate some kinds or
|
||||||
@@ -17,7 +18,7 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if filter.Limit < 0 {
|
if filter.Limit < 0 {
|
||||||
return
|
return fmt.Errorf("filter invalidated")
|
||||||
}
|
}
|
||||||
|
|
||||||
// then check if we'll reject this filter (we apply this after overwriting
|
// then check if we'll reject this filter (we apply this after overwriting
|
||||||
@@ -27,7 +28,7 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
|||||||
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))
|
ws.WriteJSON(nostr.NoticeEnvelope(msg))
|
||||||
return
|
return fmt.Errorf(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +53,8 @@ func (rl *Relay) handleRequest(ctx context.Context, id string, eose *sync.WaitGr
|
|||||||
eose.Done()
|
eose.Done()
|
||||||
}(ch)
|
}(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
|
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
|
||||||
|
|||||||
21
utils.go
21
utils.go
@@ -2,20 +2,8 @@ package khatru
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"hash/maphash"
|
|
||||||
"regexp"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
AUTH_CONTEXT_KEY = iota
|
|
||||||
WS_KEY = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
var nip20prefixmatcher = regexp.MustCompile(`^\w+: `)
|
|
||||||
|
|
||||||
func GetConnection(ctx context.Context) *WebSocket {
|
func GetConnection(ctx context.Context) *WebSocket {
|
||||||
return ctx.Value(WS_KEY).(*WebSocket)
|
return ctx.Value(WS_KEY).(*WebSocket)
|
||||||
}
|
}
|
||||||
@@ -27,12 +15,3 @@ func GetAuthed(ctx context.Context) string {
|
|||||||
}
|
}
|
||||||
return authedPubkey.(string)
|
return authedPubkey.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pointerHasher[V any](_ maphash.Seed, k *V) uint64 {
|
|
||||||
return uint64(uintptr(unsafe.Pointer(k)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOlder(previous, next *nostr.Event) bool {
|
|
||||||
return previous.CreatedAt < next.CreatedAt ||
|
|
||||||
(previous.CreatedAt == next.CreatedAt && previous.ID > next.ID)
|
|
||||||
}
|
|
||||||
|
|||||||
10
websocket.go
10
websocket.go
@@ -1,6 +1,7 @@
|
|||||||
package khatru
|
package khatru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/fasthttp/websocket"
|
"github.com/fasthttp/websocket"
|
||||||
@@ -10,10 +11,13 @@ type WebSocket struct {
|
|||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
|
||||||
|
// original request
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
// nip42
|
// nip42
|
||||||
Challenge string
|
Challenge string
|
||||||
Authed string
|
AuthedPublicKey string
|
||||||
WaitingForAuth chan struct{}
|
Authed chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebSocket) WriteJSON(any any) error {
|
func (ws *WebSocket) WriteJSON(any any) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user