From f50b7b0f8dcb93785c11a71078b5e10b21625253 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 16 Apr 2026 11:56:33 -0300 Subject: [PATCH] khatru: list clients and client details. --- khatru/relay.go | 86 +++++++++++++++++++++++++++++++++++++++++++++ khatru/websocket.go | 10 ++++++ 2 files changed, 96 insertions(+) diff --git a/khatru/relay.go b/khatru/relay.go index 9e22b0b..107137b 100644 --- a/khatru/relay.go +++ b/khatru/relay.go @@ -2,6 +2,8 @@ package khatru import ( "context" + "encoding/base64" + "encoding/binary" "iter" "log" "net/http" @@ -9,6 +11,7 @@ import ( "strconv" "strings" "time" + "unsafe" "fiatjaf.com/lib/channelmutex" "fiatjaf.com/nostr" @@ -212,6 +215,89 @@ func (rl *Relay) Stats() (clients, listeners int) { return len(rl.clients), listeners } +type ClientInfo struct { + ID string + IP string + UserAgent string + Origin string + Authenticated []nostr.PubKey + SubscriptionCount int +} + +type SubscriptionInfo struct { + ID string + Filter nostr.Filter +} + +type ClientSnapshot struct { + ClientInfo + Subscriptions []SubscriptionInfo +} + +func (rl *Relay) ListClients() []ClientInfo { + rl.clientsMutex.Lock() + defer rl.clientsMutex.Unlock() + + clients := make([]ClientInfo, 0, len(rl.clients)) + for ws, specs := range rl.clients { + clients = append(clients, ClientInfo{ + ID: ws.GetID(), + IP: GetIPFromRequest(ws.Request), + UserAgent: ws.Request.UserAgent(), + Origin: ws.Request.Header.Get("Origin"), + Authenticated: ws.AuthedPublicKeys, + SubscriptionCount: len(specs), + }) + } + + return clients +} + +func (rl *Relay) GetClientSnapshot(id string) (ClientSnapshot, bool) { + rl.clientsMutex.Lock() + defer rl.clientsMutex.Unlock() + + ptrn, err := base64.RawURLEncoding.DecodeString(id) + if err != nil { + return ClientSnapshot{}, false + } + ptr := binary.LittleEndian.Uint64(ptrn) + + // DANGEROUS: + // don't try to do anything with this `ws` object before we confirm it exists by checking the rl.clients map + ws := (*WebSocket)(unsafe.Pointer(uintptr(ptr))) + specs, ok := rl.clients[ws] + if !ok { + return ClientSnapshot{}, false + } + + details := ClientSnapshot{ + ClientInfo: ClientInfo{ + ID: id, + IP: GetIPFromRequest(ws.Request), + UserAgent: ws.Request.UserAgent(), + Origin: ws.Request.Header.Get("Origin"), + Authenticated: ws.AuthedPublicKeys, + SubscriptionCount: len(specs), + }, + Subscriptions: make([]SubscriptionInfo, 0, len(specs)), + } + + for _, spec := range specs { + filter := nostr.Filter{} + if sub, ok := rl.dispatcher.subscriptions.Load(spec.ssid); ok { + filter = sub.filter + } + + details.Subscriptions = append(details.Subscriptions, SubscriptionInfo{ + ID: spec.sid, + Filter: filter, + }) + } + + return details, true +} + func (rl *Relay) Router() *http.ServeMux { return rl.serveMux } diff --git a/khatru/websocket.go b/khatru/websocket.go index 40484c6..f90f235 100644 --- a/khatru/websocket.go +++ b/khatru/websocket.go @@ -2,9 +2,12 @@ package khatru import ( "context" + "encoding/base64" + "encoding/binary" "fmt" "net/http" "sync" + "unsafe" "fiatjaf.com/nostr" "github.com/fasthttp/websocket" @@ -31,6 +34,13 @@ type WebSocket struct { negentropySessions *xsync.MapOf[string, *NegentropySession] } +func (ws *WebSocket) GetID() string { + ptr := uintptr(unsafe.Pointer(ws)) + var id [8]byte + binary.LittleEndian.PutUint64(id[:], uint64(ptr)) + return base64.RawURLEncoding.EncodeToString(id[:]) +} + func (ws *WebSocket) WriteJSON(any any) error { if ws == nil { return fmt.Errorf("connection doesn't exist")