mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-08 22:46:48 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad92d0b051 | ||
|
|
728417852e | ||
|
|
3c1b062eb8 | ||
|
|
84d01dc1d3 | ||
|
|
888ac8c1c0 | ||
|
|
e1fd6aaa56 | ||
|
|
386a89676a |
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/fiatjaf/eventstore/lmdb"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/plugins"
|
||||
"github.com/fiatjaf/khatru/policies"
|
||||
"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, plugins.PreventTooManyIndexableTags(10))
|
||||
relay.RejectFilter = append(relay.RejectFilter, plugins.NoComplexFilters)
|
||||
relay.RejectEvent = append(relay.RejectEvent, policies.PreventTooManyIndexableTags(10))
|
||||
relay.RejectFilter = append(relay.RejectFilter, policies.NoComplexFilters)
|
||||
|
||||
relay.OnEventSaved = append(relay.OnEventSaved, func(ctx context.Context, event *nostr.Event) {
|
||||
})
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.21.0
|
||||
require (
|
||||
github.com/fasthttp/websocket v1.5.3
|
||||
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/rs/cors v1.7.0
|
||||
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.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.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM=
|
||||
github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
|
||||
github.com/nbd-wtf/go-nostr v0.26.0 h1:Tofbs9i8DD5iEKIhLlWFO7kfWpvmUG16fEyW30MzHVQ=
|
||||
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
135
handlers.go
135
handlers.go
@@ -44,9 +44,10 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
rand.Read(challenge)
|
||||
|
||||
ws := &WebSocket{
|
||||
conn: conn,
|
||||
Challenge: hex.EncodeToString(challenge),
|
||||
WaitingForAuth: make(chan struct{}),
|
||||
conn: conn,
|
||||
Request: r,
|
||||
Challenge: hex.EncodeToString(challenge),
|
||||
Authed: make(chan struct{}),
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, WS_KEY, ws)
|
||||
@@ -94,137 +95,87 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
go func(message []byte) {
|
||||
ctx = context.Background()
|
||||
ctx := context.WithValue(context.Background(), WS_KEY, ws)
|
||||
|
||||
var request []json.RawMessage
|
||||
if err := json.Unmarshal(message, &request); err != nil {
|
||||
envelope := nostr.ParseMessage(message)
|
||||
if envelope == nil {
|
||||
// stop silently
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
switch env := envelope.(type) {
|
||||
case *nostr.EventEnvelope:
|
||||
// check id
|
||||
hash := sha256.Sum256(evt.Serialize())
|
||||
hash := sha256.Sum256(env.Event.Serialize())
|
||||
id := hex.EncodeToString(hash[:])
|
||||
if id != evt.ID {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "invalid: id is computed incorrectly"})
|
||||
if id != env.Event.ID {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "invalid: id is computed incorrectly"})
|
||||
return
|
||||
}
|
||||
|
||||
// check signature
|
||||
if ok, err := evt.CheckSignature(); err != nil {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "error: failed to verify 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"})
|
||||
return
|
||||
} 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
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if evt.Kind == 5 {
|
||||
err = rl.handleDeleteRequest(ctx, &evt)
|
||||
if env.Event.Kind == 5 {
|
||||
err = rl.handleDeleteRequest(ctx, &env.Event)
|
||||
} else {
|
||||
err = rl.AddEvent(ctx, &evt)
|
||||
err = rl.AddEvent(ctx, &env.Event)
|
||||
}
|
||||
|
||||
var reason string
|
||||
if err == nil {
|
||||
ok = true
|
||||
} else {
|
||||
reason = err.Error()
|
||||
reason = nostr.NormalizeOKMessage(err.Error(), "blocked")
|
||||
}
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: ok, Reason: reason})
|
||||
case "COUNT":
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: ok, Reason: reason})
|
||||
case *nostr.CountEnvelope:
|
||||
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
|
||||
}
|
||||
|
||||
var id string
|
||||
json.Unmarshal(request[1], &id)
|
||||
if id == "" {
|
||||
ws.WriteJSON(nostr.NoticeEnvelope("COUNT has no <id>"))
|
||||
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
|
||||
}
|
||||
total += rl.handleCountRequest(ctx, ws, filters[i])
|
||||
for _, filter := range env.Filters {
|
||||
total += rl.handleCountRequest(ctx, ws, filter)
|
||||
}
|
||||
|
||||
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)
|
||||
ws.WriteJSON(nostr.CountEnvelope{SubscriptionID: env.SubscriptionID, Count: &total})
|
||||
case *nostr.ReqEnvelope:
|
||||
eose := sync.WaitGroup{}
|
||||
eose.Add(len(request[2:]))
|
||||
eose.Add(len(env.Filters))
|
||||
|
||||
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
|
||||
for _, filter := range env.Filters {
|
||||
err := rl.handleRequest(ctx, env.SubscriptionID, &eose, ws, filter)
|
||||
if err == nil {
|
||||
reason := nostr.NormalizeOKMessage(err.Error(), "blocked")
|
||||
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: reason})
|
||||
return
|
||||
}
|
||||
|
||||
go rl.handleRequest(ctx, id, &eose, ws, filters[i])
|
||||
}
|
||||
|
||||
go func() {
|
||||
eose.Wait()
|
||||
ws.WriteJSON(nostr.EOSEEnvelope(id))
|
||||
ws.WriteJSON(nostr.EOSEEnvelope(env.SubscriptionID))
|
||||
}()
|
||||
|
||||
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":
|
||||
setListener(env.SubscriptionID, ws, env.Filters)
|
||||
case *nostr.CloseEnvelope:
|
||||
removeListenerId(ws, string(*env))
|
||||
case *nostr.AuthEnvelope:
|
||||
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)
|
||||
if pubkey, ok := nip42.ValidateAuthEvent(&env.Event, ws.Challenge, rl.ServiceURL); ok {
|
||||
ws.AuthedPublicKey = pubkey
|
||||
close(ws.Authed)
|
||||
ctx = context.WithValue(ctx, AUTH_CONTEXT_KEY, pubkey)
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: true})
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: true})
|
||||
} else {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "error: failed to authenticate"})
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: false, Reason: "error: failed to authenticate"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ func RejectKind04Snoopers(ctx context.Context, filter nostr.Filter) (bool, strin
|
||||
senders := filter.Authors
|
||||
receivers, _ := filter.Tags["p"]
|
||||
switch {
|
||||
case ws.Authed == "":
|
||||
case ws.AuthedPublicKey == "":
|
||||
// not authenticated
|
||||
return true, "restricted: this relay does not serve kind-4 to unauthenticated users, does your client implement NIP-42?"
|
||||
case len(senders) == 1 && len(receivers) < 2 && (senders[0] == ws.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)
|
||||
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)
|
||||
return false, ""
|
||||
default:
|
||||
|
||||
@@ -2,12 +2,13 @@ package khatru
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"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()
|
||||
|
||||
// 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 {
|
||||
return
|
||||
return fmt.Errorf("filter invalidated")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if reject, msg := reject(ctx, filter); reject {
|
||||
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()
|
||||
}(ch)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rl *Relay) handleCountRequest(ctx context.Context, ws *WebSocket, filter nostr.Filter) int64 {
|
||||
|
||||
10
websocket.go
10
websocket.go
@@ -1,6 +1,7 @@
|
||||
package khatru
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
@@ -10,10 +11,13 @@ type WebSocket struct {
|
||||
conn *websocket.Conn
|
||||
mutex sync.Mutex
|
||||
|
||||
// original request
|
||||
Request *http.Request
|
||||
|
||||
// nip42
|
||||
Challenge string
|
||||
Authed string
|
||||
WaitingForAuth chan struct{}
|
||||
Challenge string
|
||||
AuthedPublicKey string
|
||||
Authed chan struct{}
|
||||
}
|
||||
|
||||
func (ws *WebSocket) WriteJSON(any any) error {
|
||||
|
||||
Reference in New Issue
Block a user