mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-07 14:06:51 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.PubKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
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
|
||||
store := make(map[string]*nostr.Event, 120)
|
||||
|
||||
29
handlers.go
29
handlers.go
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip42"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
// ServeHTTP implements http.Handler interface.
|
||||
@@ -21,7 +22,7 @@ func (rl *Relay) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
rl.HandleWebsocket(w, r)
|
||||
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
||||
rl.HandleNIP11(w, r)
|
||||
cors.AllowAll().Handler(http.HandlerFunc(rl.HandleNIP11)).ServeHTTP(w, r)
|
||||
} else {
|
||||
rl.serveMux.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -48,6 +49,8 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
WaitingForAuth: make(chan struct{}),
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, WS_KEY, ws)
|
||||
|
||||
// reader
|
||||
go func() {
|
||||
defer func() {
|
||||
@@ -75,6 +78,7 @@ 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
|
||||
@@ -115,14 +119,15 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// check serialization
|
||||
serialized := evt.Serialize()
|
||||
// check id
|
||||
hash := sha256.Sum256(evt.Serialize())
|
||||
id := hex.EncodeToString(hash[:])
|
||||
if id != evt.ID {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "invalid: id is computed incorrectly"})
|
||||
return
|
||||
}
|
||||
|
||||
// assign ID
|
||||
hash := sha256.Sum256(serialized)
|
||||
evt.ID = hex.EncodeToString(hash[:])
|
||||
|
||||
// check signature (requires the ID to be set)
|
||||
// check signature
|
||||
if ok, err := evt.CheckSignature(); err != nil {
|
||||
ws.WriteJSON(nostr.OKEnvelope{EventID: evt.ID, OK: false, Reason: "error: failed to verify signature"})
|
||||
return
|
||||
@@ -251,5 +256,11 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
package plugins
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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
|
||||
// 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) {
|
||||
if ignore(event.Kind) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
ntags := 0
|
||||
for _, tag := range event.Tags {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
@@ -64,7 +84,7 @@ func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Even
|
||||
}
|
||||
|
||||
// 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 true, "event kind not allowed"
|
||||
@@ -1,4 +1,4 @@
|
||||
package plugins
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"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) {
|
||||
items := len(filter.Tags) + len(filter.Kinds)
|
||||
|
||||
@@ -17,6 +18,7 @@ func NoComplexFilters(ctx context.Context, filter nostr.Filter) (reject bool, ms
|
||||
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 {
|
||||
@@ -28,6 +30,13 @@ func NoEmptyFilters(ctx context.Context, filter nostr.Filter) (reject bool, msg
|
||||
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"
|
||||
@@ -1,4 +1,4 @@
|
||||
package plugins
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
"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,
|
||||
// only when authentication is a thing
|
||||
if !slices.Contains(filter.Kinds, 4) {
|
||||
34
relay.go
34
relay.go
@@ -18,8 +18,9 @@ func NewRelay() *Relay {
|
||||
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
|
||||
|
||||
Info: &nip11.RelayInformationDocument{
|
||||
Software: "https://github.com/fiatjaf/khatru",
|
||||
Version: "n/a",
|
||||
Software: "https://github.com/fiatjaf/khatru",
|
||||
Version: "n/a",
|
||||
SupportedNIPs: make([]int, 0),
|
||||
},
|
||||
|
||||
upgrader: websocket.Upgrader{
|
||||
@@ -41,20 +42,21 @@ func NewRelay() *Relay {
|
||||
type Relay struct {
|
||||
ServiceURL string // required for nip-42
|
||||
|
||||
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)
|
||||
OverwriteFilter []func(ctx context.Context, filter *nostr.Filter)
|
||||
OverwriteCountFilter []func(ctx context.Context, filter *nostr.Filter)
|
||||
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)
|
||||
OnAuth []func(ctx context.Context, pubkey string)
|
||||
OnConnect []func(ctx context.Context)
|
||||
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
||||
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)
|
||||
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)
|
||||
OnAuth []func(ctx context.Context, pubkey string)
|
||||
OnConnect []func(ctx context.Context)
|
||||
OnEventSaved []func(ctx context.Context, event *nostr.Event)
|
||||
|
||||
// editing info will affect
|
||||
Info *nip11.RelayInformationDocument
|
||||
|
||||
Reference in New Issue
Block a user