khatru: support multi-user auth.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -76,6 +77,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
conn: conn,
|
conn: conn,
|
||||||
Request: r,
|
Request: r,
|
||||||
Challenge: rl.ChallengePrefix + hex.EncodeToString(challenge),
|
Challenge: rl.ChallengePrefix + hex.EncodeToString(challenge),
|
||||||
|
AuthedPublicKeys: make([]nostr.PubKey, 0),
|
||||||
negentropySessions: xsync.NewMapOf[string, *NegentropySession](),
|
negentropySessions: xsync.NewMapOf[string, *NegentropySession](),
|
||||||
}
|
}
|
||||||
ws.Context, ws.cancel = context.WithCancel(context.Background())
|
ws.Context, ws.cancel = context.WithCancel(context.Background())
|
||||||
@@ -317,11 +319,22 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
case *nostr.AuthEnvelope:
|
case *nostr.AuthEnvelope:
|
||||||
wsBaseUrl := strings.Replace(rl.getBaseURL(r), "http", "ws", 1)
|
wsBaseUrl := strings.Replace(rl.getBaseURL(r), "http", "ws", 1)
|
||||||
if pubkey, ok := nip42.ValidateAuthEvent(env.Event, ws.Challenge, wsBaseUrl); ok {
|
if pubkey, ok := nip42.ValidateAuthEvent(env.Event, ws.Challenge, wsBaseUrl); ok {
|
||||||
ws.AuthedPublicKey = pubkey
|
|
||||||
|
total := len(ws.AuthedPublicKeys) - 1
|
||||||
ws.authLock.Lock()
|
ws.authLock.Lock()
|
||||||
if ws.Authed != nil {
|
if idx := slices.Index(ws.AuthedPublicKeys, pubkey); idx == -1 {
|
||||||
close(ws.Authed)
|
// this public key is not authenticated
|
||||||
ws.Authed = nil
|
if total < rl.MaxAuthenticatedClients {
|
||||||
|
// add it to the end (the last pubkey is the one we'll use in a single-user context)
|
||||||
|
ws.AuthedPublicKeys = append(ws.AuthedPublicKeys, pubkey)
|
||||||
|
} else {
|
||||||
|
// remove the first (oldest) and add the new pubkey to the end
|
||||||
|
ws.AuthedPublicKeys[0] = ws.AuthedPublicKeys[total-1]
|
||||||
|
ws.AuthedPublicKeys[total-1] = pubkey
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this is already authed, so move it to the end
|
||||||
|
ws.AuthedPublicKeys[idx], ws.AuthedPublicKeys[total-1] = ws.AuthedPublicKeys[total-1], ws.AuthedPublicKeys[idx]
|
||||||
}
|
}
|
||||||
ws.authLock.Unlock()
|
ws.authLock.Unlock()
|
||||||
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: true})
|
ws.WriteJSON(nostr.OKEnvelope{EventID: env.Event.ID, OK: true})
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func NewRelay() *Relay {
|
|||||||
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
|
Log: log.New(os.Stderr, "[khatru-relay] ", log.LstdFlags),
|
||||||
|
|
||||||
Info: &nip11.RelayInformationDocument{
|
Info: &nip11.RelayInformationDocument{
|
||||||
Software: "https://fiatjaf.com/nostr/khatru",
|
Software: "https://pkg.go.dev/fiatjaf.com/nostr/khatru",
|
||||||
Version: "n/a",
|
Version: "n/a",
|
||||||
SupportedNIPs: []any{1, 11, 40, 42, 70, 86},
|
SupportedNIPs: []any{1, 11, 40, 42, 70, 86},
|
||||||
},
|
},
|
||||||
@@ -46,6 +46,8 @@ func NewRelay() *Relay {
|
|||||||
PongWait: 60 * time.Second,
|
PongWait: 60 * time.Second,
|
||||||
PingPeriod: 30 * time.Second,
|
PingPeriod: 30 * time.Second,
|
||||||
MaxMessageSize: 512000,
|
MaxMessageSize: 512000,
|
||||||
|
|
||||||
|
MaxAuthenticatedClients: 32,
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.expirationManager = newExpirationManager(rl)
|
rl.expirationManager = newExpirationManager(rl)
|
||||||
@@ -112,10 +114,11 @@ type Relay struct {
|
|||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
|
|
||||||
// websocket options
|
// websocket options
|
||||||
WriteWait time.Duration // Time allowed to write a message to the peer.
|
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.
|
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.
|
PingPeriod time.Duration // Send pings to peer with this period. Must be less than pongWait.
|
||||||
MaxMessageSize int64 // Maximum message size allowed from peer.
|
MaxMessageSize int64 // Maximum message size allowed from peer.
|
||||||
|
MaxAuthenticatedClients int
|
||||||
|
|
||||||
// NIP-40 expiration manager
|
// NIP-40 expiration manager
|
||||||
expirationManager *expirationManager
|
expirationManager *expirationManager
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ const (
|
|||||||
|
|
||||||
func RequestAuth(ctx context.Context) {
|
func RequestAuth(ctx context.Context) {
|
||||||
ws := GetConnection(ctx)
|
ws := GetConnection(ctx)
|
||||||
ws.authLock.Lock()
|
|
||||||
if ws.Authed == nil {
|
|
||||||
ws.Authed = make(chan struct{})
|
|
||||||
}
|
|
||||||
ws.authLock.Unlock()
|
|
||||||
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
|
ws.WriteJSON(nostr.AuthEnvelope{Challenge: &ws.Challenge})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +26,16 @@ func GetConnection(ctx context.Context) *WebSocket {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAuthed returns the last pubkey to have authenticated. Returns false if no one has.
|
||||||
|
//
|
||||||
|
// In a NIP-86 context it returns the single pubkey that have authenticated for that specific method call.
|
||||||
func GetAuthed(ctx context.Context) (nostr.PubKey, bool) {
|
func GetAuthed(ctx context.Context) (nostr.PubKey, bool) {
|
||||||
if conn := GetConnection(ctx); conn != nil {
|
if conn := GetConnection(ctx); conn != nil {
|
||||||
return conn.AuthedPublicKey, conn.AuthedPublicKey != nostr.ZeroPK
|
total := len(conn.AuthedPublicKeys)
|
||||||
|
if total == 0 {
|
||||||
|
return nostr.ZeroPK, false
|
||||||
|
}
|
||||||
|
return conn.AuthedPublicKeys[total-1], true
|
||||||
}
|
}
|
||||||
if nip86Auth := ctx.Value(nip86HeaderAuthKey); nip86Auth != nil {
|
if nip86Auth := ctx.Value(nip86HeaderAuthKey); nip86Auth != nil {
|
||||||
return nip86Auth.(nostr.PubKey), true
|
return nip86Auth.(nostr.PubKey), true
|
||||||
@@ -41,6 +43,23 @@ func GetAuthed(ctx context.Context) (nostr.PubKey, bool) {
|
|||||||
return nostr.ZeroPK, false
|
return nostr.ZeroPK, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAuthed checks if the given public key is among the multiple that may have potentially authenticated.
|
||||||
|
func IsAuthed(ctx context.Context, pubkey nostr.PubKey) bool {
|
||||||
|
if conn := GetConnection(ctx); conn != nil {
|
||||||
|
for _, pk := range conn.AuthedPublicKeys {
|
||||||
|
if pk == pubkey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nip86Auth := ctx.Value(nip86HeaderAuthKey); nip86Auth != nil {
|
||||||
|
return nip86Auth.(nostr.PubKey) == pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// IsInternalCall returns true when a call to QueryEvents, for example, is being made because of a deletion
|
// IsInternalCall returns true when a call to QueryEvents, for example, is being made because of a deletion
|
||||||
// or expiration request.
|
// or expiration request.
|
||||||
func IsInternalCall(ctx context.Context) bool {
|
func IsInternalCall(ctx context.Context) bool {
|
||||||
|
|||||||
@@ -22,14 +22,12 @@ type WebSocket struct {
|
|||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
// nip42
|
// nip42
|
||||||
Challenge string
|
Challenge string
|
||||||
AuthedPublicKey nostr.PubKey
|
AuthedPublicKeys []nostr.PubKey
|
||||||
Authed chan struct{}
|
authLock sync.Mutex
|
||||||
|
|
||||||
// nip77
|
// nip77
|
||||||
negentropySessions *xsync.MapOf[string, *NegentropySession]
|
negentropySessions *xsync.MapOf[string, *NegentropySession]
|
||||||
|
|
||||||
authLock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebSocket) WriteJSON(any any) error {
|
func (ws *WebSocket) WriteJSON(any any) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user