implement fully working listener/subscribe/unsubscribe publisher
This commit is contained in:
@@ -40,6 +40,7 @@ type C struct {
|
||||
SpiderSeeds []string `env:"ORLY_SPIDER_SEEDS" usage:"seeds to use for the spider (relays that are looked up initially to find owner relay lists) (comma separated)" default:"wss://relay.nostr.band/,wss://relay.damus.io/,wss://nostr.wine/,wss://nostr.land/,wss://theforest.nostr1.com/"`
|
||||
Owners []string `env:"ORLY_OWNERS" usage:"list of users whose follow lists designate whitelisted users who can publish events, and who can read if public readable is false (comma separated)"`
|
||||
Private bool `env:"ORLY_PRIVATE" usage:"do not spider for user metadata because the relay is private and this would leak relay memberships" default:"false"`
|
||||
Whitelist []string `env:"ORLY_WHITELIST" usage:"only allow connections from this list of IP addresses"`
|
||||
}
|
||||
|
||||
// New creates and initializes a new configuration object for the relay
|
||||
@@ -90,6 +91,17 @@ func New() (cfg *C, err error) {
|
||||
lol.SetLogLevel(cfg.LogLevel)
|
||||
log.I.F("loaded configuration from %s", envPath)
|
||||
}
|
||||
// if spider seeds has no elements, there still is a single entry with an
|
||||
// empty string; and also if any of the fields are empty strings, they need
|
||||
// to be removed.
|
||||
var seeds []string
|
||||
for _, u := range cfg.SpiderSeeds {
|
||||
if u == "" {
|
||||
continue
|
||||
}
|
||||
seeds = append(seeds, u)
|
||||
}
|
||||
cfg.SpiderSeeds = seeds
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
10
pkg/app/relay/config.go
Normal file
10
pkg/app/relay/config.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/app/config"
|
||||
)
|
||||
|
||||
func (s *Server) Config() (c *config.C) {
|
||||
c = s.C
|
||||
return
|
||||
}
|
||||
@@ -6,6 +6,8 @@ package publish
|
||||
import (
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/interfaces/publisher"
|
||||
"orly.dev/pkg/interfaces/typer"
|
||||
"orly.dev/pkg/utils/log"
|
||||
)
|
||||
|
||||
// S is the control structure for the subscription management scheme.
|
||||
@@ -24,13 +26,14 @@ var _ publisher.I = &S{}
|
||||
func (s *S) Type() string { return "publish" }
|
||||
|
||||
func (s *S) Deliver(ev *event.E) {
|
||||
log.I.F("number of publishers: %d", len(s.Publishers))
|
||||
for _, p := range s.Publishers {
|
||||
log.I.F("delivering to subscriber type %s", p.Type())
|
||||
p.Deliver(ev)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *S) Receive(msg publisher.Message) {
|
||||
func (s *S) Receive(msg typer.T) {
|
||||
t := msg.Type()
|
||||
for _, p := range s.Publishers {
|
||||
if p.Type() == t {
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"orly.dev/pkg/protocol/openapi"
|
||||
"orly.dev/pkg/protocol/socketapi"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"orly.dev/pkg/app/config"
|
||||
@@ -101,7 +103,7 @@ func NewServer(
|
||||
C: sp.C,
|
||||
Lists: new(Lists),
|
||||
}
|
||||
s.listeners = publish.New(socketapi.New(s))
|
||||
s.listeners = publish.New(socketapi.New(s), openapi.NewPublisher(s))
|
||||
go func() {
|
||||
if err := s.relay.Init(); chk.E(err) {
|
||||
s.Shutdown()
|
||||
@@ -133,6 +135,21 @@ func NewServer(
|
||||
//
|
||||
// - For all other paths, delegates to the internal mux's ServeHTTP method.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c := s.Config()
|
||||
remote := helpers.GetRemoteFromReq(r)
|
||||
var whitelisted bool
|
||||
if len(c.Whitelist) > 0 {
|
||||
for _, addr := range c.Whitelist {
|
||||
if strings.HasPrefix(remote, addr) {
|
||||
whitelisted = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
whitelisted = true
|
||||
}
|
||||
if !whitelisted {
|
||||
return
|
||||
}
|
||||
// standard nostr protocol only governs the "root" path of the relay and
|
||||
// websockets
|
||||
if r.URL.Path == "/" {
|
||||
|
||||
@@ -100,7 +100,7 @@ func (s *Server) SpiderFetch(
|
||||
|
||||
log.I.F("%d events found of type %s", len(pkKindMap), kindsList)
|
||||
|
||||
if !noFetch {
|
||||
if !noFetch && len(s.C.SpiderSeeds) > 0 {
|
||||
// we need to search the spider seeds.
|
||||
// Break up pubkeys into batches of 128
|
||||
for i := 0; i < len(pubkeys); i += 128 {
|
||||
|
||||
@@ -2,16 +2,13 @@ package publisher
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/interfaces/typer"
|
||||
)
|
||||
|
||||
type Message interface {
|
||||
Type() string
|
||||
}
|
||||
|
||||
type I interface {
|
||||
Message
|
||||
typer.T
|
||||
Deliver(ev *event.E)
|
||||
Receive(msg Message)
|
||||
Receive(msg typer.T)
|
||||
}
|
||||
|
||||
type Publishers []I
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/pkg/app/config"
|
||||
"orly.dev/pkg/app/relay/publish"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/filters"
|
||||
@@ -39,4 +40,5 @@ type I interface {
|
||||
PublicReadable() bool
|
||||
ServiceURL(req *http.Request) (s string)
|
||||
OwnersPubkeys() (pks [][]byte)
|
||||
Config() *config.C
|
||||
}
|
||||
|
||||
123
pkg/protocol/openapi/listen.go
Normal file
123
pkg/protocol/openapi/listen.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/sse"
|
||||
"net/http"
|
||||
"orly.dev/pkg/app/relay/helpers"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
SubID string `json:"sub_id"`
|
||||
Event *event.J `json:"event"`
|
||||
}
|
||||
|
||||
type ListenInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
|
||||
Accept string `header:"Accept" default:"text/event-stream" enum:"text/event-stream" required:"true"`
|
||||
}
|
||||
|
||||
func (x *Operations) RegisterListen(api huma.API) {
|
||||
name := "Listen"
|
||||
description := `Opens up a HTTP SSE subscription channel that will send results from the /subscribe endpoint. Writes the subscription channel identifier as the first event in the stream.
|
||||
|
||||
Close the connection to end all deliveries, or use /unsubscribe. Before /subscribe or /unsubscribe can be used, this must be first opened.
|
||||
|
||||
Many browsers have a limited number of SSE channels that can be open at once, so this allows an app to consolidate all of its subscriptions into one. If the client understands HTTP/2 this limit is relaxed from 6 concurrent subscription channels per domain to 100, the net/http library has supported the protocol transparently since Go 1.6. However, because of the design of this API, each instance of a client only requires one open HTTP SSE connection to receive all subscriptions.`
|
||||
path := x.path + "/listen"
|
||||
scopes := []string{"user", "read"}
|
||||
method := http.MethodPost
|
||||
sse.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
},
|
||||
map[string]any{
|
||||
"client_id": "",
|
||||
"event": &Event{},
|
||||
},
|
||||
func(ctx context.T, input *ListenInput, send sse.Sender) {
|
||||
r := ctx.Value("http-request").(*http.Request)
|
||||
remote := helpers.GetRemoteFromReq(r)
|
||||
var err error
|
||||
var authed bool
|
||||
var pubkey []byte
|
||||
if x.I.AuthRequired() && !x.I.PublicReadable() {
|
||||
authed, pubkey = x.UserAuth(r, remote)
|
||||
if !authed {
|
||||
err = huma.Error401Unauthorized("Not Authorized")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a unique client ID
|
||||
id := make([]byte, 16)
|
||||
if _, err = rand.Read(id); chk.E(err) {
|
||||
return
|
||||
}
|
||||
clientId := hex.Enc(id)
|
||||
|
||||
// Create a receiver channel for events
|
||||
receiver := make(DeliverChan, 32)
|
||||
|
||||
// Create and register the listener
|
||||
listener := &H{
|
||||
Id: clientId,
|
||||
New: true,
|
||||
Receiver: receiver,
|
||||
Pubkey: pubkey,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
}
|
||||
|
||||
log.T.F("creating new listener %s", clientId)
|
||||
x.Publisher().Receive(listener)
|
||||
|
||||
// Send the client ID as the first event
|
||||
if err = send.Data(clientId); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Event loop
|
||||
out:
|
||||
for {
|
||||
select {
|
||||
case <-x.Context().Done():
|
||||
// server shutdown
|
||||
break out
|
||||
case <-r.Context().Done():
|
||||
// connection has closed
|
||||
break out
|
||||
case ev := <-receiver:
|
||||
// if the channel is closed, the event will be nil
|
||||
if ev == nil {
|
||||
break out
|
||||
}
|
||||
if err = send.Data(
|
||||
Event{
|
||||
clientId, ev.Event.ToEventJ(),
|
||||
},
|
||||
); chk.E(err) {
|
||||
break out
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clean up the listener when the context is done
|
||||
log.T.F("removing listener %s", clientId)
|
||||
listener.Cancel = true
|
||||
x.Publisher().Receive(listener)
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
317
pkg/protocol/openapi/publisher.go
Normal file
317
pkg/protocol/openapi/publisher.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/interfaces/publisher"
|
||||
"orly.dev/pkg/interfaces/server"
|
||||
"orly.dev/pkg/interfaces/typer"
|
||||
"orly.dev/pkg/protocol/auth"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const Type = "openapi"
|
||||
|
||||
type Delivery struct {
|
||||
SubId string `json:"sub_id"`
|
||||
Event *event.E `json:"event"`
|
||||
}
|
||||
|
||||
type DeliverChan chan *Delivery
|
||||
|
||||
type H struct {
|
||||
sync.Mutex
|
||||
|
||||
// If Cancel is true, this is a close command (must be done when a listener
|
||||
// connection is closed).
|
||||
Cancel bool
|
||||
|
||||
// New is a flag that signifies a newly created client_id
|
||||
New bool
|
||||
|
||||
// Id is the identifier for an HTTP subscription listener channel
|
||||
Id string
|
||||
|
||||
// FilterMap is the collection of filters associated with a listener.
|
||||
FilterMap map[string]*filter.F
|
||||
|
||||
// Receiver is the channel for receiving events
|
||||
Receiver DeliverChan
|
||||
|
||||
// Pubkey is the authenticated public key for this listener
|
||||
Pubkey []byte
|
||||
}
|
||||
|
||||
func (h *H) Type() (typeName string) { return Type }
|
||||
|
||||
type Publisher struct {
|
||||
sync.Mutex
|
||||
|
||||
// ListenMap maps listener IDs to listener objects
|
||||
ListenMap map[string]*H
|
||||
|
||||
// Server is an interface to the server
|
||||
Server server.I
|
||||
}
|
||||
|
||||
var _ publisher.I = &Publisher{}
|
||||
|
||||
func (p *Publisher) Type() (typeName string) { return Type }
|
||||
|
||||
func NewPublisher(s server.I) (p *Publisher) {
|
||||
return &Publisher{
|
||||
ListenMap: make(map[string]*H),
|
||||
Server: s,
|
||||
}
|
||||
}
|
||||
|
||||
// Receive handles incoming messages to manage HTTP listener subscriptions and
|
||||
// associated filters.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - msg (typer.T): The incoming message to process; expected to be of
|
||||
// type *H to trigger subscription management actions.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// - Checks if the message is of type *H.
|
||||
//
|
||||
// - If Cancel is true, removes a subscriber by ID or the entire listener.
|
||||
//
|
||||
// - Otherwise, adds the subscription to the map under a mutex lock.
|
||||
//
|
||||
// - Logs actions related to subscription creation or removal.
|
||||
func (p *Publisher) Receive(msg typer.T) {
|
||||
if m, ok := msg.(*H); ok {
|
||||
if m.Cancel {
|
||||
if m.Id == "" {
|
||||
// Can't do anything with an empty ID
|
||||
log.W.F("received cancel request with empty ID")
|
||||
return
|
||||
}
|
||||
|
||||
if m.FilterMap == nil || len(m.FilterMap) == 0 {
|
||||
// Remove the entire listener
|
||||
p.removeListener(m.Id)
|
||||
log.T.F("removed listener %s", m.Id)
|
||||
} else {
|
||||
// Remove specific subscriptions
|
||||
p.removeSubscription(m.Id, m.FilterMap)
|
||||
for id := range m.FilterMap {
|
||||
log.T.F("removed subscription %s for %s", id, m.Id)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
if listener, ok := p.ListenMap[m.Id]; !ok {
|
||||
// Don't create new listeners automatically
|
||||
if m.New {
|
||||
// Create a new listener when New flag is set
|
||||
listener := &H{
|
||||
Id: m.Id,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
Receiver: m.Receiver,
|
||||
Pubkey: m.Pubkey,
|
||||
}
|
||||
|
||||
// Add the filters if provided
|
||||
if m.FilterMap != nil {
|
||||
for id, f := range m.FilterMap {
|
||||
listener.FilterMap[id] = f
|
||||
log.T.F("added subscription %s for new listener %s", id, m.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the listener to the map
|
||||
p.ListenMap[m.Id] = listener
|
||||
log.T.F("added new listener %s", m.Id)
|
||||
} else {
|
||||
// Only the Listen API should create new listeners
|
||||
log.W.F("received message for non-existent listener %s", m.Id)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
// Update existing listener
|
||||
if m.FilterMap != nil {
|
||||
for id, f := range m.FilterMap {
|
||||
listener.FilterMap[id] = f
|
||||
log.T.F("added subscription %s for %s", id, m.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deliver processes and distributes an event to all matching subscribers based
|
||||
// on their filter configurations.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - ev (*event.E): The event to be delivered to subscribed clients.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// Delivers the event to all subscribers whose filters match the event. It
|
||||
// applies authentication checks if required by the server, and skips delivery
|
||||
// for unauthenticated users when events are privileged.
|
||||
func (p *Publisher) Deliver(ev *event.E) {
|
||||
log.T.F("delivering event %0x to HTTP subscribers", ev.ID)
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
for listenerId, listener := range p.ListenMap {
|
||||
for subId, filter := range listener.FilterMap {
|
||||
if !filter.Matches(ev) {
|
||||
log.I.F(
|
||||
"listener %s, subscription id %s event\n%s\ndoes not match filter\n%s",
|
||||
listenerId, subId, ev.Marshal(nil),
|
||||
filter.Marshal(nil),
|
||||
)
|
||||
continue
|
||||
}
|
||||
if p.Server.AuthRequired() {
|
||||
if !auth.CheckPrivilege(listener.Pubkey, ev) {
|
||||
log.W.F(
|
||||
"not privileged %0x ev pubkey %0x listener pubkey %0x kind %s privileged: %v",
|
||||
listener.Pubkey, ev.Pubkey,
|
||||
listener.Pubkey, ev.Kind.Name(),
|
||||
ev.Kind.IsPrivileged(),
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Send the event to the listener's receiver channel
|
||||
select {
|
||||
case listener.Receiver <- &Delivery{SubId: subId, Event: ev}:
|
||||
log.T.F(
|
||||
"dispatched event %0x to subscription %s for listener %s",
|
||||
ev.ID, subId, listenerId,
|
||||
)
|
||||
default:
|
||||
log.W.F(
|
||||
"failed to dispatch event %0x to subscription %s for listener %s: channel full",
|
||||
ev.ID, subId, listenerId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeListener removes a listener from the Publisher collection.
|
||||
func (p *Publisher) removeListener(id string) {
|
||||
p.Lock()
|
||||
delete(p.ListenMap, id)
|
||||
p.Unlock()
|
||||
}
|
||||
|
||||
// removeSubscription removes specific subscriptions from a listener.
|
||||
// It does not delete the listener even if all subscriptions are removed.
|
||||
func (p *Publisher) removeSubscription(
|
||||
listenerId string, filterMap map[string]*filter.F,
|
||||
) {
|
||||
p.Lock()
|
||||
if listener, ok := p.ListenMap[listenerId]; ok {
|
||||
for id := range filterMap {
|
||||
delete(listener.FilterMap, id)
|
||||
}
|
||||
// We no longer delete the listener when all subscriptions are removed
|
||||
// This allows the listener to remain active for future subscriptions
|
||||
}
|
||||
p.Unlock()
|
||||
}
|
||||
|
||||
// ListenerExists checks if a listener with the given ID exists.
|
||||
func (p *Publisher) ListenerExists(id string) bool {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
_, exists := p.ListenMap[id]
|
||||
return exists
|
||||
}
|
||||
|
||||
// SubscriptionExists checks if a subscription with the given ID exists for a specific listener.
|
||||
func (p *Publisher) SubscriptionExists(listenerId string, subscriptionId string) bool {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
listener, exists := p.ListenMap[listenerId]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
_, exists = listener.FilterMap[subscriptionId]
|
||||
return exists
|
||||
}
|
||||
|
||||
// CheckListenerExists is a package-level function that checks if a listener exists.
|
||||
// This function is used by the Subscribe and Unsubscribe APIs to check if a client ID exists.
|
||||
func CheckListenerExists(clientId string, publishers ...publisher.I) bool {
|
||||
for _, p := range publishers {
|
||||
// Check if the publisher is of type *Publisher
|
||||
if pub, ok := p.(*Publisher); ok {
|
||||
if pub.ListenerExists(clientId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the publisher has a Publishers field of type publisher.Publishers
|
||||
// This handles the case where the publisher is a *publish.S
|
||||
val := reflect.ValueOf(p)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
if val.Kind() == reflect.Struct {
|
||||
field := val.FieldByName("Publishers")
|
||||
if field.IsValid() && field.Type().String() == "publisher.Publishers" {
|
||||
// Iterate through the publishers
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
pub := field.Index(i).Interface().(publisher.I)
|
||||
// Check if this publisher is of type *Publisher
|
||||
if openPub, ok := pub.(*Publisher); ok {
|
||||
if openPub.ListenerExists(clientId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckSubscriptionExists is a package-level function that checks if a subscription exists for a specific listener.
|
||||
// This function is used by the Unsubscribe API to check if a subscription ID exists before attempting to unsubscribe.
|
||||
func CheckSubscriptionExists(clientId string, subscriptionId string, publishers ...publisher.I) bool {
|
||||
for _, p := range publishers {
|
||||
// Check if the publisher is of type *Publisher
|
||||
if pub, ok := p.(*Publisher); ok {
|
||||
if pub.SubscriptionExists(clientId, subscriptionId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the publisher has a Publishers field of type publisher.Publishers
|
||||
// This handles the case where the publisher is a *publish.S
|
||||
val := reflect.ValueOf(p)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
if val.Kind() == reflect.Struct {
|
||||
field := val.FieldByName("Publishers")
|
||||
if field.IsValid() && field.Type().String() == "publisher.Publishers" {
|
||||
// Iterate through the publishers
|
||||
for i := 0; i < field.Len(); i++ {
|
||||
pub := field.Index(i).Interface().(publisher.I)
|
||||
// Check if this publisher is of type *Publisher
|
||||
if openPub, ok := pub.(*Publisher); ok {
|
||||
if openPub.SubscriptionExists(clientId, subscriptionId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
431
pkg/protocol/openapi/publisher_test.go
Normal file
431
pkg/protocol/openapi/publisher_test.go
Normal file
@@ -0,0 +1,431 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/pkg/app/config"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"orly.dev/pkg/app/relay/publish"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/encoders/filters"
|
||||
"orly.dev/pkg/encoders/kind"
|
||||
"orly.dev/pkg/encoders/kinds"
|
||||
"orly.dev/pkg/encoders/tags"
|
||||
"orly.dev/pkg/interfaces/relay"
|
||||
"orly.dev/pkg/interfaces/store"
|
||||
ctx "orly.dev/pkg/utils/context"
|
||||
)
|
||||
|
||||
// mockServer implements the server.I interface for testing
|
||||
type mockServer struct {
|
||||
authRequired bool
|
||||
context ctx.T
|
||||
}
|
||||
|
||||
// Implement the methods needed for our tests
|
||||
func (m *mockServer) AuthRequired() bool {
|
||||
return m.authRequired
|
||||
}
|
||||
|
||||
func (m *mockServer) Context() ctx.T {
|
||||
return m.context
|
||||
}
|
||||
|
||||
func (m *mockServer) Publisher() *publish.S {
|
||||
return nil // Not used in our tests
|
||||
}
|
||||
|
||||
// Stub implementations for the rest of the server.I interface
|
||||
func (m *mockServer) AcceptEvent(
|
||||
c ctx.T, ev *event.E, hr *http.Request, authedPubkey []byte,
|
||||
remote string,
|
||||
) (accept bool, notice string, afterSave func()) {
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
func (m *mockServer) AcceptReq(
|
||||
c ctx.T, hr *http.Request, f *filters.T,
|
||||
authedPubkey []byte, remote string,
|
||||
) (allowed *filters.T, accept bool, modified bool) {
|
||||
return f, true, false
|
||||
}
|
||||
|
||||
func (m *mockServer) AddEvent(
|
||||
c ctx.T, rl relay.I, ev *event.E, hr *http.Request, origin string,
|
||||
) (accepted bool, message []byte) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *mockServer) AdminAuth(
|
||||
r *http.Request, remote string, tolerance ...time.Duration,
|
||||
) (authed bool, pubkey []byte) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *mockServer) UserAuth(
|
||||
r *http.Request, remote string, tolerance ...time.Duration,
|
||||
) (authed bool, pubkey []byte) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *mockServer) Publish(c ctx.T, evt *event.E) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockServer) Relay() relay.I {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockServer) Shutdown() {}
|
||||
|
||||
func (m *mockServer) Storage() store.I {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockServer) PublicReadable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockServer) ServiceURL(req *http.Request) (s string) {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockServer) OwnersPubkeys() (pks [][]byte) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockServer) Config() (c *config.C) {
|
||||
return
|
||||
}
|
||||
|
||||
// TestPublisherFunctionality tests the listen/subscribe/unsubscribe and publisher functionality
|
||||
func TestPublisherFunctionality(t *testing.T) {
|
||||
// Create a context with cancel function
|
||||
testCtx, cancel := ctx.Cancel(ctx.Bg())
|
||||
defer cancel()
|
||||
|
||||
// Create a mock server
|
||||
mockServer := &mockServer{
|
||||
authRequired: false,
|
||||
context: testCtx,
|
||||
}
|
||||
|
||||
// Create a publisher
|
||||
publisher := NewPublisher(mockServer)
|
||||
|
||||
// Test 1: Register a listener
|
||||
t.Run(
|
||||
"RegisterListener", func(t *testing.T) {
|
||||
// Create a receiver channel
|
||||
receiver := make(event.C, 32)
|
||||
|
||||
// Create a listener
|
||||
listener := &H{
|
||||
Id: "test-listener",
|
||||
Receiver: receiver,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
}
|
||||
|
||||
// Register the listener
|
||||
publisher.Receive(listener)
|
||||
|
||||
// Verify the listener was registered
|
||||
if _, ok := publisher.ListenMap["test-listener"]; !ok {
|
||||
t.Errorf("Listener was not registered")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Test 2: Add a subscription
|
||||
t.Run(
|
||||
"AddSubscription", func(t *testing.T) {
|
||||
// Create a filter
|
||||
f := &filter.F{}
|
||||
|
||||
// Create a subscription
|
||||
subscription := &H{
|
||||
Id: "test-listener",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"test-subscription": f,
|
||||
},
|
||||
}
|
||||
|
||||
// Add the subscription
|
||||
publisher.Receive(subscription)
|
||||
|
||||
// Verify the subscription was added
|
||||
listener, ok := publisher.ListenMap["test-listener"]
|
||||
if !ok {
|
||||
t.Errorf("Listener not found")
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := listener.FilterMap["test-subscription"]; !ok {
|
||||
t.Errorf("Subscription was not added")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Test 3: Deliver an event
|
||||
t.Run(
|
||||
"DeliverEvent", func(t *testing.T) {
|
||||
// Create an event that matches the filter
|
||||
ev := &event.E{
|
||||
Kind: kind.TextNote,
|
||||
}
|
||||
|
||||
// Deliver the event
|
||||
publisher.Deliver(ev)
|
||||
|
||||
// Get the listener
|
||||
listener, ok := publisher.ListenMap["test-listener"]
|
||||
if !ok {
|
||||
t.Errorf("Listener not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the event was received
|
||||
select {
|
||||
case receivedEv := <-listener.Receiver:
|
||||
if receivedEv != ev {
|
||||
t.Errorf("Received event does not match delivered event")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Errorf("Event was not received within timeout")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Test 4: Unsubscribe
|
||||
t.Run(
|
||||
"Unsubscribe", func(t *testing.T) {
|
||||
// Create a new listener first since the previous one was removed
|
||||
receiver := make(event.C, 32)
|
||||
listener := &H{
|
||||
Id: "test-listener",
|
||||
Receiver: receiver,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
}
|
||||
publisher.Receive(listener)
|
||||
|
||||
// Add a subscription
|
||||
subscription := &H{
|
||||
Id: "test-listener",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"test-subscription": &filter.F{},
|
||||
},
|
||||
}
|
||||
publisher.Receive(subscription)
|
||||
|
||||
// Create an unsubscribe message
|
||||
unsubscribe := &H{
|
||||
Id: "test-listener",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"test-subscription": nil,
|
||||
},
|
||||
Cancel: true,
|
||||
}
|
||||
|
||||
// Unsubscribe
|
||||
publisher.Receive(unsubscribe)
|
||||
|
||||
// Verify the listener was removed (since it had no more subscriptions)
|
||||
if _, ok := publisher.ListenMap["test-listener"]; ok {
|
||||
t.Errorf("Listener was not removed, but should be removed when all subscriptions are gone")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Test 5: Remove listener
|
||||
t.Run(
|
||||
"RemoveListener", func(t *testing.T) {
|
||||
// Create a remove listener message
|
||||
removeListener := &H{
|
||||
Id: "test-listener",
|
||||
Cancel: true,
|
||||
}
|
||||
|
||||
// Remove the listener
|
||||
publisher.Receive(removeListener)
|
||||
|
||||
// Verify the listener was removed
|
||||
if _, ok := publisher.ListenMap["test-listener"]; ok {
|
||||
t.Errorf("Listener was not removed")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Test 6: Edge case - Unsubscribe non-existent subscription
|
||||
t.Run(
|
||||
"UnsubscribeNonExistentSubscription", func(t *testing.T) {
|
||||
// Create a new listener first
|
||||
receiver := make(event.C, 32)
|
||||
listener := &H{
|
||||
Id: "test-listener-2",
|
||||
Receiver: receiver,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
}
|
||||
publisher.Receive(listener)
|
||||
|
||||
// Add a subscription to ensure the listener has at least one subscription
|
||||
subscription := &H{
|
||||
Id: "test-listener-2",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"existing-subscription": &filter.F{},
|
||||
},
|
||||
}
|
||||
publisher.Receive(subscription)
|
||||
|
||||
// Create an unsubscribe message for a non-existent subscription
|
||||
unsubscribe := &H{
|
||||
Id: "test-listener-2",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"non-existent-subscription": nil,
|
||||
},
|
||||
Cancel: true,
|
||||
}
|
||||
|
||||
// Unsubscribe
|
||||
publisher.Receive(unsubscribe)
|
||||
|
||||
// Verify the listener still exists (since it still has one subscription)
|
||||
if _, ok := publisher.ListenMap["test-listener-2"]; !ok {
|
||||
t.Errorf("Listener was removed, but should still exist since it has other subscriptions")
|
||||
}
|
||||
|
||||
// Verify the existing subscription is still there
|
||||
listener, ok := publisher.ListenMap["test-listener-2"]
|
||||
if !ok {
|
||||
t.Errorf("Listener not found")
|
||||
return
|
||||
}
|
||||
if _, ok := listener.FilterMap["existing-subscription"]; !ok {
|
||||
t.Errorf("Existing subscription was removed")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Test 7: Edge case - Deliver event with authentication required
|
||||
t.Run(
|
||||
"DeliverEventWithAuthRequired", func(t *testing.T) {
|
||||
// Set auth required to true
|
||||
mockServer.authRequired = true
|
||||
|
||||
// Create a new listener with pubkey
|
||||
receiver := make(event.C, 32)
|
||||
listener := &H{
|
||||
Id: "test-listener-3",
|
||||
Receiver: receiver,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
Pubkey: []byte("test-pubkey"),
|
||||
}
|
||||
publisher.Receive(listener)
|
||||
|
||||
// Add a subscription
|
||||
subscription := &H{
|
||||
Id: "test-listener-3",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"test-subscription-3": &filter.F{},
|
||||
},
|
||||
}
|
||||
publisher.Receive(subscription)
|
||||
|
||||
// Create an event with a different pubkey and a privileged kind
|
||||
ev := &event.E{
|
||||
Kind: kind.EncryptedDirectMessage,
|
||||
Pubkey: []byte("different-pubkey"),
|
||||
Tags: tags.New(), // Initialize empty tags
|
||||
}
|
||||
|
||||
// Deliver the event
|
||||
publisher.Deliver(ev)
|
||||
|
||||
// Verify the event was not received (due to auth check)
|
||||
select {
|
||||
case <-listener.Receiver:
|
||||
t.Errorf("Event was received, but should have been blocked by auth check")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// This is expected - no event should be received
|
||||
}
|
||||
|
||||
// Reset auth required
|
||||
mockServer.authRequired = false
|
||||
},
|
||||
)
|
||||
|
||||
// Test 8: Filter matching - Events are only delivered to listeners with matching filters
|
||||
t.Run(
|
||||
"FilterMatching", func(t *testing.T) {
|
||||
// Create two listeners with different filters
|
||||
receiver1 := make(event.C, 32)
|
||||
listener1 := &H{
|
||||
Id: "test-listener-filter-1",
|
||||
Receiver: receiver1,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
}
|
||||
publisher.Receive(listener1)
|
||||
|
||||
receiver2 := make(event.C, 32)
|
||||
listener2 := &H{
|
||||
Id: "test-listener-filter-2",
|
||||
Receiver: receiver2,
|
||||
FilterMap: make(map[string]*filter.F),
|
||||
}
|
||||
publisher.Receive(listener2)
|
||||
|
||||
// Add different filters to each listener
|
||||
// First filter matches events with kind.TextNote
|
||||
filter1 := &filter.F{
|
||||
Kinds: kinds.New(kind.TextNote),
|
||||
}
|
||||
subscription1 := &H{
|
||||
Id: "test-listener-filter-1",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"filter-subscription-1": filter1,
|
||||
},
|
||||
}
|
||||
publisher.Receive(subscription1)
|
||||
|
||||
// Second filter matches events with kind.EncryptedDirectMessage
|
||||
filter2 := &filter.F{
|
||||
Kinds: kinds.New(kind.EncryptedDirectMessage),
|
||||
}
|
||||
subscription2 := &H{
|
||||
Id: "test-listener-filter-2",
|
||||
FilterMap: map[string]*filter.F{
|
||||
"filter-subscription-2": filter2,
|
||||
},
|
||||
}
|
||||
publisher.Receive(subscription2)
|
||||
|
||||
// Create an event that matches only the first filter
|
||||
ev := &event.E{
|
||||
Kind: kind.TextNote,
|
||||
Tags: tags.New(),
|
||||
}
|
||||
|
||||
// Deliver the event
|
||||
publisher.Deliver(ev)
|
||||
|
||||
// Verify the event was received by the first listener
|
||||
select {
|
||||
case receivedEv := <-receiver1:
|
||||
if receivedEv != ev {
|
||||
t.Errorf("Received event does not match delivered event")
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Errorf("Event was not received by first listener within timeout")
|
||||
}
|
||||
|
||||
// Verify the event was NOT received by the second listener
|
||||
select {
|
||||
case <-receiver2:
|
||||
t.Errorf("Event was received by second listener, but should not have matched its filter")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// This is expected - no event should be received
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
82
pkg/protocol/openapi/subscribe.go
Normal file
82
pkg/protocol/openapi/subscribe.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"net/http"
|
||||
"orly.dev/pkg/app/relay/helpers"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
)
|
||||
|
||||
type SubscribeInput struct {
|
||||
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
|
||||
Accept string `header:"Accept" default:"application/nostr+json"`
|
||||
ClientId string `path:"client_id" doc:"Client identifier code associated with subscription channel created with /listen"`
|
||||
Id string `path:"id" doc:"Identifier of the subscription associated with the filter"`
|
||||
Body *Filter `doc:"filter JSON (standard NIP-01 filter syntax)"`
|
||||
}
|
||||
|
||||
func (x *Operations) RegisterSubscribe(api huma.API) {
|
||||
name := "Subscribe"
|
||||
description := `Create a new subscription based on a provided filter that will return new events that have arrived matching the subscription filter, over the HTTP SSE channel identified by client_id, created by the /listen endpoint.`
|
||||
path := x.path + "/subscribe/{client_id}/{id}"
|
||||
scopes := []string{"user", "read"}
|
||||
method := http.MethodPost
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
RequestBody: EventsBody,
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
}, func(ctx context.T, input *SubscribeInput) (
|
||||
output *struct{}, err error,
|
||||
) {
|
||||
// Validate client_id exists
|
||||
if input.ClientId == "" {
|
||||
return nil, huma.Error400BadRequest("client_id is required")
|
||||
}
|
||||
|
||||
// Validate subscription ID exists
|
||||
if input.Id == "" {
|
||||
return nil, huma.Error400BadRequest("subscription id is required")
|
||||
}
|
||||
|
||||
// Validate filter exists
|
||||
if input.Body == nil {
|
||||
return nil, huma.Error400BadRequest("filter is required")
|
||||
}
|
||||
|
||||
// Check if the client ID exists
|
||||
if !CheckListenerExists(input.ClientId, x.Publisher()) {
|
||||
return nil, huma.Error404NotFound("client_id does not exist, create a listener first with the /listen endpoint")
|
||||
}
|
||||
|
||||
// Convert the Filter to a filter.F
|
||||
f := input.Body.ToFilter()
|
||||
|
||||
// Create a subscription message
|
||||
subscription := &H{
|
||||
Id: input.ClientId,
|
||||
FilterMap: map[string]*filter.F{
|
||||
input.Id: f,
|
||||
},
|
||||
}
|
||||
|
||||
// Send the subscription to the publisher. The publisher will route
|
||||
// it to the appropriate handler based on Type()
|
||||
x.Publisher().Receive(subscription)
|
||||
|
||||
log.T.F(
|
||||
"added subscription %s for listener %s\nfilter %s", input.Id,
|
||||
input.ClientId, f.Marshal(nil),
|
||||
)
|
||||
|
||||
return
|
||||
},
|
||||
)
|
||||
}
|
||||
77
pkg/protocol/openapi/unsubscribe.go
Normal file
77
pkg/protocol/openapi/unsubscribe.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"net/http"
|
||||
"orly.dev/pkg/app/relay/helpers"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
)
|
||||
|
||||
type UnsubscribeInput struct {
|
||||
ClientId string `path:"client_id" doc:"Client identifier code associated with subscription channel created with /listen"`
|
||||
Id string `path:"id" doc:"Identifier of the subscription to cancel"`
|
||||
}
|
||||
|
||||
func (x *Operations) RegisterUnsubscribe(api huma.API) {
|
||||
name := "Unsubscribe"
|
||||
description := `Cancel a subscription with the matching subscription identifier, attached to the identified client_id associated with the HTTP SSE connection.`
|
||||
path := x.path + "/unsubscribe/{client_id}/{id}"
|
||||
scopes := []string{"user", "read"}
|
||||
method := http.MethodPost
|
||||
huma.Register(
|
||||
api, huma.Operation{
|
||||
OperationID: name,
|
||||
Summary: name,
|
||||
Path: path,
|
||||
Method: method,
|
||||
Tags: []string{"events"},
|
||||
Description: helpers.GenerateDescription(description, scopes),
|
||||
Security: []map[string][]string{{"auth": scopes}},
|
||||
}, func(ctx context.T, input *UnsubscribeInput) (
|
||||
output *struct{}, err error,
|
||||
) {
|
||||
// Validate client_id exists
|
||||
if input.ClientId == "" {
|
||||
return nil, huma.Error400BadRequest("client_id is required")
|
||||
}
|
||||
|
||||
// Validate subscription ID exists
|
||||
if input.Id == "" {
|
||||
return nil, huma.Error400BadRequest("subscription id is required")
|
||||
}
|
||||
|
||||
// Check if the client ID exists
|
||||
if !CheckListenerExists(input.ClientId, x.Publisher()) {
|
||||
return nil, huma.Error404NotFound("client_id doesn't exist, create a listener first with the /listen endpoint")
|
||||
}
|
||||
|
||||
// Check if the subscription ID exists
|
||||
if !CheckSubscriptionExists(
|
||||
input.ClientId, input.Id, x.Publisher(),
|
||||
) {
|
||||
return nil, huma.Error404NotFound("subscription id doesn't exist for this client")
|
||||
}
|
||||
|
||||
// Create a cancel subscription message
|
||||
unsubscribe := &H{
|
||||
Id: input.ClientId,
|
||||
FilterMap: map[string]*filter.F{
|
||||
input.Id: nil, // We only need the key, not the value
|
||||
},
|
||||
Cancel: true, // Set Cancel to true to remove the subscription
|
||||
}
|
||||
|
||||
// Send the unsubscribe message to the publisher. The publisher will route it to the appropriate handler based on Type()
|
||||
x.Publisher().Receive(unsubscribe)
|
||||
|
||||
log.T.F(
|
||||
"removed subscription %s for listener %s", input.Id,
|
||||
input.ClientId,
|
||||
)
|
||||
|
||||
return &struct{}{}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -129,6 +129,7 @@ func (a *A) HandleEvent(
|
||||
}
|
||||
return
|
||||
}
|
||||
log.I.F("checking if policy allows this event")
|
||||
// check that relay policy allows this event
|
||||
accept, notice, _ := srv.AcceptEvent(
|
||||
c, env.E, a.Listener.Request, a.Listener.AuthedPubkey(),
|
||||
@@ -145,6 +146,7 @@ func (a *A) HandleEvent(
|
||||
}
|
||||
return
|
||||
}
|
||||
log.I.F("checking for protected tag")
|
||||
// check for protected tag (NIP-70)
|
||||
protectedTag := env.E.Tags.GetFirst(tag.New("-"))
|
||||
if protectedTag != nil && a.AuthRequired() {
|
||||
|
||||
@@ -26,7 +26,8 @@ import (
|
||||
// corresponding handler method, generates a notice for errors or unknown types,
|
||||
// logs the notice, and writes it back to the listener if required.
|
||||
func (a *A) HandleMessage(msg, authedPubkey []byte) {
|
||||
log.T.F("%s received message:\n%s", a.Listener.RealRemote(), string(msg))
|
||||
remote := a.Listener.RealRemote()
|
||||
log.T.F("%s received message:\n%s", remote, string(msg))
|
||||
var notice []byte
|
||||
var err error
|
||||
var t string
|
||||
|
||||
@@ -76,7 +76,8 @@ func (a *A) HandleReq(c context.T, req []byte, srv server.I) (r []byte) {
|
||||
return
|
||||
}
|
||||
if !a.I.PublicReadable() {
|
||||
// send a notice in case the client renders it to explain why auth is required
|
||||
// send a notice in case the client renders it to explain why auth
|
||||
// is required
|
||||
opks := a.I.OwnersPubkeys()
|
||||
var npubList string
|
||||
for i, pk := range opks {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"orly.dev/pkg/encoders/filters"
|
||||
"orly.dev/pkg/interfaces/publisher"
|
||||
"orly.dev/pkg/interfaces/server"
|
||||
"orly.dev/pkg/interfaces/typer"
|
||||
"orly.dev/pkg/protocol/auth"
|
||||
"orly.dev/pkg/protocol/ws"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
@@ -81,7 +82,7 @@ func (p *S) Type() (typeName string) { return Type }
|
||||
// - Otherwise, adds the subscription to the map under a mutex lock.
|
||||
//
|
||||
// - Logs actions related to subscription creation or removal.
|
||||
func (p *S) Receive(msg publisher.Message) {
|
||||
func (p *S) Receive(msg typer.T) {
|
||||
if m, ok := msg.(*W); ok {
|
||||
if m.Cancel {
|
||||
if m.Id == "" {
|
||||
@@ -127,18 +128,24 @@ func (p *S) Receive(msg publisher.Message) {
|
||||
// applies authentication checks if required by the server, and skips delivery
|
||||
// for unauthenticated users when events are privileged.
|
||||
func (p *S) Deliver(ev *event.E) {
|
||||
log.T.F("delivering event %0x to subscribers", ev.ID)
|
||||
var err error
|
||||
p.Mx.Lock()
|
||||
defer p.Mx.Unlock()
|
||||
log.T.F(
|
||||
"delivering event %0x to websocket subscribers %d", ev.ID, len(p.Map),
|
||||
)
|
||||
for w, subs := range p.Map {
|
||||
// log.I.F("%v %s", subs, w.RealRemote())
|
||||
log.I.F("%v %s", subs, w.RealRemote())
|
||||
for id, subscriber := range subs {
|
||||
// log.T.F(
|
||||
// "subscriber %s\n%s", w.RealRemote(),
|
||||
// subscriber.Marshal(nil),
|
||||
// )
|
||||
log.T.F(
|
||||
"subscriber %s\n%s", w.RealRemote(),
|
||||
subscriber.Marshal(nil),
|
||||
)
|
||||
if !subscriber.Match(ev) {
|
||||
log.I.F(
|
||||
"subscriber %s filter %s not match", id,
|
||||
subscriber.Marshal(nil),
|
||||
)
|
||||
continue
|
||||
}
|
||||
if p.Server.AuthRequired() {
|
||||
|
||||
@@ -54,6 +54,22 @@ type A struct {
|
||||
// resources on connection termination or cancellation, adhering to the given
|
||||
// context's lifecycle.
|
||||
func (a *A) Serve(w http.ResponseWriter, r *http.Request, s server.I) {
|
||||
c := a.Config()
|
||||
remote := helpers.GetRemoteFromReq(r)
|
||||
var whitelisted bool
|
||||
if len(c.Whitelist) > 0 {
|
||||
for _, addr := range c.Whitelist {
|
||||
if strings.HasPrefix(remote, addr) {
|
||||
whitelisted = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
whitelisted = true
|
||||
}
|
||||
if !whitelisted {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
ticker := time.NewTicker(DefaultPingWait)
|
||||
var cancel context.F
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.2.20
|
||||
v0.3.0
|
||||
Reference in New Issue
Block a user