You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
322 lines
8.0 KiB
322 lines
8.0 KiB
package nrc |
|
|
|
import ( |
|
"context" |
|
"encoding/json" |
|
"sync" |
|
"time" |
|
) |
|
|
|
const ( |
|
// DefaultSessionTimeout is the default inactivity timeout for sessions. |
|
DefaultSessionTimeout = 30 * time.Minute |
|
// DefaultMaxSubscriptions is the default maximum subscriptions per session. |
|
DefaultMaxSubscriptions = 100 |
|
) |
|
|
|
// Session represents an NRC client session through the tunnel. |
|
type Session struct { |
|
// ID is the unique session identifier. |
|
ID string |
|
// ClientPubkey is the public key of the connected client. |
|
ClientPubkey []byte |
|
// ConversationKey is the NIP-44 conversation key for this session. |
|
ConversationKey []byte |
|
// DeviceName is the optional device identifier. |
|
DeviceName string |
|
// AuthMode is the authentication mode used. |
|
AuthMode AuthMode |
|
|
|
// CreatedAt is when the session was created. |
|
CreatedAt time.Time |
|
// LastActivity is the timestamp of the last activity. |
|
LastActivity time.Time |
|
|
|
// subscriptions maps client subscription IDs to internal subscription state. |
|
subscriptions map[string]*Subscription |
|
// subMu protects the subscriptions map. |
|
subMu sync.RWMutex |
|
|
|
// ctx is the session context. |
|
ctx context.Context |
|
// cancel cancels the session context. |
|
cancel context.CancelFunc |
|
|
|
// eventCh receives events from the local relay for this session. |
|
eventCh chan *SessionEvent |
|
} |
|
|
|
// Subscription represents a tunneled subscription. |
|
type Subscription struct { |
|
// ID is the client's subscription ID. |
|
ID string |
|
// CreatedAt is when the subscription was created. |
|
CreatedAt time.Time |
|
// EventCount tracks how many events have been sent. |
|
EventCount int64 |
|
// EOSESent indicates whether EOSE has been sent. |
|
EOSESent bool |
|
} |
|
|
|
// SessionEvent wraps a relay response for delivery to the client. |
|
type SessionEvent struct { |
|
// Type is the response type (EVENT, OK, EOSE, NOTICE, CLOSED, COUNT, AUTH). |
|
Type string |
|
// Payload is the response payload array. |
|
Payload []any |
|
// RequestEventID is the ID of the request event this responds to (if applicable). |
|
RequestEventID string |
|
} |
|
|
|
// NewSession creates a new session. |
|
func NewSession(id string, clientPubkey, conversationKey []byte, authMode AuthMode, deviceName string) *Session { |
|
ctx, cancel := context.WithCancel(context.Background()) |
|
now := time.Now() |
|
return &Session{ |
|
ID: id, |
|
ClientPubkey: clientPubkey, |
|
ConversationKey: conversationKey, |
|
DeviceName: deviceName, |
|
AuthMode: authMode, |
|
CreatedAt: now, |
|
LastActivity: now, |
|
subscriptions: make(map[string]*Subscription), |
|
ctx: ctx, |
|
cancel: cancel, |
|
eventCh: make(chan *SessionEvent, 100), |
|
} |
|
} |
|
|
|
// Context returns the session's context. |
|
func (s *Session) Context() context.Context { |
|
return s.ctx |
|
} |
|
|
|
// Close closes the session and cleans up resources. |
|
func (s *Session) Close() { |
|
s.cancel() |
|
close(s.eventCh) |
|
} |
|
|
|
// Events returns the channel for receiving events destined for this session. |
|
func (s *Session) Events() <-chan *SessionEvent { |
|
return s.eventCh |
|
} |
|
|
|
// SendEvent sends an event to the session's event channel. |
|
func (s *Session) SendEvent(ev *SessionEvent) bool { |
|
select { |
|
case s.eventCh <- ev: |
|
return true |
|
case <-s.ctx.Done(): |
|
return false |
|
default: |
|
// Channel full, drop event |
|
return false |
|
} |
|
} |
|
|
|
// Touch updates the last activity timestamp. |
|
func (s *Session) Touch() { |
|
s.LastActivity = time.Now() |
|
} |
|
|
|
// IsExpired checks if the session has been inactive too long. |
|
func (s *Session) IsExpired(timeout time.Duration) bool { |
|
return time.Since(s.LastActivity) > timeout |
|
} |
|
|
|
// AddSubscription adds a new subscription to the session. |
|
func (s *Session) AddSubscription(subID string) error { |
|
s.subMu.Lock() |
|
defer s.subMu.Unlock() |
|
|
|
if len(s.subscriptions) >= DefaultMaxSubscriptions { |
|
return ErrTooManySubscriptions |
|
} |
|
|
|
s.subscriptions[subID] = &Subscription{ |
|
ID: subID, |
|
CreatedAt: time.Now(), |
|
} |
|
return nil |
|
} |
|
|
|
// RemoveSubscription removes a subscription from the session. |
|
func (s *Session) RemoveSubscription(subID string) { |
|
s.subMu.Lock() |
|
defer s.subMu.Unlock() |
|
delete(s.subscriptions, subID) |
|
} |
|
|
|
// GetSubscription returns a subscription by ID. |
|
func (s *Session) GetSubscription(subID string) *Subscription { |
|
s.subMu.RLock() |
|
defer s.subMu.RUnlock() |
|
return s.subscriptions[subID] |
|
} |
|
|
|
// HasSubscription checks if a subscription exists. |
|
func (s *Session) HasSubscription(subID string) bool { |
|
s.subMu.RLock() |
|
defer s.subMu.RUnlock() |
|
_, ok := s.subscriptions[subID] |
|
return ok |
|
} |
|
|
|
// SubscriptionCount returns the number of active subscriptions. |
|
func (s *Session) SubscriptionCount() int { |
|
s.subMu.RLock() |
|
defer s.subMu.RUnlock() |
|
return len(s.subscriptions) |
|
} |
|
|
|
// MarkEOSE marks a subscription as having sent EOSE. |
|
func (s *Session) MarkEOSE(subID string) { |
|
s.subMu.Lock() |
|
defer s.subMu.Unlock() |
|
if sub, ok := s.subscriptions[subID]; ok { |
|
sub.EOSESent = true |
|
} |
|
} |
|
|
|
// IncrementEventCount increments the event count for a subscription. |
|
func (s *Session) IncrementEventCount(subID string) { |
|
s.subMu.Lock() |
|
defer s.subMu.Unlock() |
|
if sub, ok := s.subscriptions[subID]; ok { |
|
sub.EventCount++ |
|
} |
|
} |
|
|
|
// SessionManager manages multiple NRC sessions. |
|
type SessionManager struct { |
|
sessions map[string]*Session |
|
mu sync.RWMutex |
|
timeout time.Duration |
|
} |
|
|
|
// NewSessionManager creates a new session manager. |
|
func NewSessionManager(timeout time.Duration) *SessionManager { |
|
if timeout == 0 { |
|
timeout = DefaultSessionTimeout |
|
} |
|
return &SessionManager{ |
|
sessions: make(map[string]*Session), |
|
timeout: timeout, |
|
} |
|
} |
|
|
|
// Get returns a session by ID. |
|
func (m *SessionManager) Get(sessionID string) *Session { |
|
m.mu.RLock() |
|
defer m.mu.RUnlock() |
|
return m.sessions[sessionID] |
|
} |
|
|
|
// GetOrCreate gets an existing session or creates a new one. |
|
func (m *SessionManager) GetOrCreate(sessionID string, clientPubkey, conversationKey []byte, authMode AuthMode, deviceName string) *Session { |
|
m.mu.Lock() |
|
defer m.mu.Unlock() |
|
|
|
if session, ok := m.sessions[sessionID]; ok { |
|
session.Touch() |
|
return session |
|
} |
|
|
|
session := NewSession(sessionID, clientPubkey, conversationKey, authMode, deviceName) |
|
m.sessions[sessionID] = session |
|
return session |
|
} |
|
|
|
// Remove removes a session. |
|
func (m *SessionManager) Remove(sessionID string) { |
|
m.mu.Lock() |
|
defer m.mu.Unlock() |
|
|
|
if session, ok := m.sessions[sessionID]; ok { |
|
session.Close() |
|
delete(m.sessions, sessionID) |
|
} |
|
} |
|
|
|
// CleanupExpired removes expired sessions. |
|
func (m *SessionManager) CleanupExpired() int { |
|
m.mu.Lock() |
|
defer m.mu.Unlock() |
|
|
|
var removed int |
|
for id, session := range m.sessions { |
|
if session.IsExpired(m.timeout) { |
|
session.Close() |
|
delete(m.sessions, id) |
|
removed++ |
|
} |
|
} |
|
return removed |
|
} |
|
|
|
// Count returns the number of active sessions. |
|
func (m *SessionManager) Count() int { |
|
m.mu.RLock() |
|
defer m.mu.RUnlock() |
|
return len(m.sessions) |
|
} |
|
|
|
// Close closes all sessions. |
|
func (m *SessionManager) Close() { |
|
m.mu.Lock() |
|
defer m.mu.Unlock() |
|
|
|
for _, session := range m.sessions { |
|
session.Close() |
|
} |
|
m.sessions = make(map[string]*Session) |
|
} |
|
|
|
// RequestMessage represents a parsed NRC request message. |
|
type RequestMessage struct { |
|
Type string // EVENT, REQ, CLOSE, AUTH, COUNT |
|
Payload []any |
|
} |
|
|
|
// ResponseMessage represents an NRC response message to be sent. |
|
type ResponseMessage struct { |
|
Type string // EVENT, OK, EOSE, NOTICE, CLOSED, COUNT, AUTH |
|
Payload []any |
|
} |
|
|
|
// ParseRequestContent parses the decrypted content of an NRC request. |
|
func ParseRequestContent(content []byte) (*RequestMessage, error) { |
|
// Content format: {"type": "EVENT|REQ|...", "payload": [...]} |
|
// Parse as generic JSON |
|
var msg struct { |
|
Type string `json:"type"` |
|
Payload []any `json:"payload"` |
|
} |
|
|
|
if err := json.Unmarshal(content, &msg); err != nil { |
|
return nil, err |
|
} |
|
|
|
if msg.Type == "" { |
|
return nil, ErrInvalidMessageType |
|
} |
|
|
|
return &RequestMessage{ |
|
Type: msg.Type, |
|
Payload: msg.Payload, |
|
}, nil |
|
} |
|
|
|
// MarshalResponseContent marshals an NRC response for encryption. |
|
func MarshalResponseContent(resp *ResponseMessage) ([]byte, error) { |
|
msg := struct { |
|
Type string `json:"type"` |
|
Payload []any `json:"payload"` |
|
}{ |
|
Type: resp.Type, |
|
Payload: resp.Payload, |
|
} |
|
return json.Marshal(msg) |
|
}
|
|
|