180 lines
5.9 KiB
Go
180 lines
5.9 KiB
Go
package khatru
|
|
|
|
import (
|
|
"context"
|
|
"iter"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"fiatjaf.com/nostr"
|
|
"fiatjaf.com/nostr/eventstore"
|
|
"fiatjaf.com/nostr/nip11"
|
|
"fiatjaf.com/nostr/nip45/hyperloglog"
|
|
"github.com/fasthttp/websocket"
|
|
)
|
|
|
|
func NewRelay() *Relay {
|
|
ctx, cancel := context.WithCancelCause(context.Background())
|
|
|
|
rl := &Relay{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
|
|
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
|
|
|
|
Info: &nip11.RelayInformationDocument{
|
|
Software: "https://pkg.go.dev/fiatjaf.com/nostr/khatru",
|
|
Version: "n/a",
|
|
SupportedNIPs: []any{1, 11, 42, 70, 86},
|
|
},
|
|
|
|
upgrader: websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
CheckOrigin: func(r *http.Request) bool { return true },
|
|
},
|
|
|
|
clients: make(map[*WebSocket][]listenerSpec, 100),
|
|
listeners: make([]listener, 0, 100),
|
|
|
|
serveMux: &http.ServeMux{},
|
|
|
|
WriteWait: 10 * time.Second,
|
|
PongWait: 60 * time.Second,
|
|
PingPeriod: 30 * time.Second,
|
|
MaxMessageSize: 512000,
|
|
|
|
MaxAuthenticatedClients: 8,
|
|
}
|
|
|
|
return rl
|
|
}
|
|
|
|
type Relay struct {
|
|
ctx context.Context
|
|
cancel context.CancelCauseFunc
|
|
|
|
// setting this variable overwrites the hackish workaround we do to try to figure out our own base URL
|
|
ServiceURL string
|
|
|
|
// hooks that will be called at various times
|
|
OnEvent func(ctx context.Context, event nostr.Event) (reject bool, msg string)
|
|
StoreEvent func(ctx context.Context, event nostr.Event) error
|
|
ReplaceEvent func(ctx context.Context, event nostr.Event) error
|
|
DeleteEvent func(ctx context.Context, id nostr.ID) error
|
|
OnEventSaved func(ctx context.Context, event nostr.Event)
|
|
OnEphemeralEvent func(ctx context.Context, event nostr.Event)
|
|
OnRequest func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
|
OnCount func(ctx context.Context, filter nostr.Filter) (reject bool, msg string)
|
|
QueryStored func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event]
|
|
Count func(ctx context.Context, filter nostr.Filter) (uint32, error)
|
|
CountHLL func(ctx context.Context, filter nostr.Filter, offset int) (uint32, *hyperloglog.HyperLogLog, error)
|
|
RejectConnection func(r *http.Request) bool
|
|
OnConnect func(ctx context.Context)
|
|
OnDisconnect func(ctx context.Context)
|
|
OverwriteRelayInformation func(ctx context.Context, r *http.Request, info nip11.RelayInformationDocument) nip11.RelayInformationDocument
|
|
PreventBroadcast func(ws *WebSocket, event nostr.Event) bool
|
|
|
|
// this can be ignored unless you know what you're doing
|
|
ChallengePrefix string
|
|
|
|
// these are used when this relays acts as a router
|
|
routes []Route
|
|
getSubRelayFromEvent func(*nostr.Event) *Relay // used for handling EVENTs
|
|
getSubRelayFromFilter func(nostr.Filter) *Relay // used for handling REQs
|
|
|
|
// setting up handlers here will enable these methods
|
|
ManagementAPI RelayManagementAPI
|
|
|
|
// editing info will affect the NIP-11 responses
|
|
Info *nip11.RelayInformationDocument
|
|
|
|
// Default logger, as set by NewServer, is a stdlib logger prefixed with "[khatru-relay] ",
|
|
// outputting to stderr.
|
|
Log *log.Logger
|
|
|
|
// for establishing websockets
|
|
upgrader websocket.Upgrader
|
|
|
|
// keep a connection reference to all connected clients for Server.Shutdown
|
|
// also used for keeping track of who is listening to what
|
|
clients map[*WebSocket][]listenerSpec
|
|
listeners []listener
|
|
clientsMutex sync.Mutex
|
|
|
|
// set this to true to support negentropy
|
|
Negentropy bool
|
|
|
|
// in case you call Server.Start
|
|
Addr string
|
|
serveMux *http.ServeMux
|
|
httpServer *http.Server
|
|
|
|
// websocket options
|
|
WriteWait time.Duration // Time allowed to write a message to 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.
|
|
MaxMessageSize int64 // Maximum message size allowed from peer.
|
|
MaxAuthenticatedClients int
|
|
|
|
// NIP-40 expiration manager
|
|
expirationManager *expirationManager
|
|
}
|
|
|
|
// UseEventstore hooks up an eventstore.Store into the relay in the default way.
|
|
// It should be used in 85% of the cases, when you don't want to do any complicated scheme with your event storage.
|
|
//
|
|
// maxQueryLimit is the default max limit to be enforced when querying events, to prevent users for downloading way
|
|
// too much, setting it to something like 500 or 1000 should be ok in most cases.
|
|
func (rl *Relay) UseEventstore(store eventstore.Store, maxQueryLimit int) {
|
|
rl.QueryStored = func(ctx context.Context, filter nostr.Filter) iter.Seq[nostr.Event] {
|
|
return store.QueryEvents(filter, maxQueryLimit)
|
|
}
|
|
rl.Count = func(ctx context.Context, filter nostr.Filter) (uint32, error) {
|
|
return store.CountEvents(filter)
|
|
}
|
|
rl.StoreEvent = func(ctx context.Context, event nostr.Event) error {
|
|
return store.SaveEvent(event)
|
|
}
|
|
rl.ReplaceEvent = func(ctx context.Context, event nostr.Event) error {
|
|
return store.ReplaceEvent(event)
|
|
}
|
|
rl.DeleteEvent = func(ctx context.Context, id nostr.ID) error {
|
|
return store.DeleteEvent(id)
|
|
}
|
|
|
|
// only when using the eventstore we automatically set up the expiration manager
|
|
rl.StartExpirationManager(rl.QueryStored, rl.DeleteEvent)
|
|
}
|
|
|
|
func (rl *Relay) getBaseURL(r *http.Request) string {
|
|
if rl.ServiceURL != "" {
|
|
return rl.ServiceURL
|
|
}
|
|
|
|
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.Contains(host, ":") {
|
|
// 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
|
|
}
|