186 lines
5.3 KiB
Go
186 lines
5.3 KiB
Go
package mtprotoedge
|
|
|
|
import "time"
|
|
|
|
// logicalSession is the process-local MTProto session that survives physical
|
|
// transport replacement. The outbound state is the sole owner of every
|
|
// content-related server message until msgs_ack, explicit session destruction,
|
|
// or the bounded offline-retention window expires.
|
|
type logicalSession struct {
|
|
key sessionKey
|
|
outbound *outboundState
|
|
offlineAt time.Time
|
|
businessAuthKeyID [8]byte
|
|
businessAuthResolved bool
|
|
}
|
|
|
|
const logicalSessionOfflineTTL = 6 * time.Minute
|
|
|
|
func (m *SessionManager) attachLogicalSession(c *Conn, budget *outboundTrackedBudget) {
|
|
if m == nil || c == nil {
|
|
return
|
|
}
|
|
key := connSessionKey(c)
|
|
m.mu.Lock()
|
|
logical := m.logicalSessions[key]
|
|
if logical == nil {
|
|
logical = &logicalSession{
|
|
key: key,
|
|
outbound: newOutboundState(budget),
|
|
}
|
|
m.logicalSessions[key] = logical
|
|
}
|
|
logical.outbound.persistent.Store(true)
|
|
if businessAuthKeyID, resolved := c.BusinessAuthKeyID(); resolved {
|
|
logical.businessAuthKeyID = businessAuthKeyID
|
|
logical.businessAuthResolved = true
|
|
}
|
|
logical.offlineAt = time.Time{}
|
|
c.outboundState = logical.outbound
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// adoptLogicalSession is a construction-test/embedded-Conn bridge. Production
|
|
// Conns are attached before their actor starts; direct Conn builders may already
|
|
// own an actor-local state when a Server terminal callback publishes the receipt.
|
|
func (m *SessionManager) adoptLogicalSession(c *Conn) {
|
|
if m == nil || c == nil || c.outboundState == nil {
|
|
return
|
|
}
|
|
key := connSessionKey(c)
|
|
m.mu.Lock()
|
|
logical := m.logicalSessions[key]
|
|
if logical == nil {
|
|
logical = &logicalSession{key: key, outbound: c.outboundState}
|
|
m.logicalSessions[key] = logical
|
|
}
|
|
logical.outbound.persistent.Store(true)
|
|
if businessAuthKeyID, resolved := c.BusinessAuthKeyID(); resolved {
|
|
logical.businessAuthKeyID = businessAuthKeyID
|
|
logical.businessAuthResolved = true
|
|
}
|
|
logical.offlineAt = time.Time{}
|
|
// The actor's state pointer is immutable after startOutbound. Production
|
|
// attaches before start; this adoption bridge only marks that already-owned
|
|
// state persistent and must never write the Conn field concurrently.
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
func (m *SessionManager) markLogicalSessionOfflineLocked(key sessionKey, now time.Time) {
|
|
logical := m.logicalSessions[key]
|
|
if logical == nil || m.bySession[key] != nil || m.claims[key] != nil {
|
|
return
|
|
}
|
|
if logical.offlineAt.IsZero() {
|
|
logical.offlineAt = now
|
|
}
|
|
}
|
|
|
|
func (m *SessionManager) destroyLogicalSessionLocked(key sessionKey) *outboundState {
|
|
logical := m.logicalSessions[key]
|
|
if logical == nil {
|
|
return nil
|
|
}
|
|
delete(m.logicalSessions, key)
|
|
return logical.outbound
|
|
}
|
|
|
|
func (m *SessionManager) bindLogicalSessionAuthKeyLocked(key sessionKey, businessAuthKeyID [8]byte) {
|
|
if logical := m.logicalSessions[key]; logical != nil {
|
|
logical.businessAuthKeyID = businessAuthKeyID
|
|
logical.businessAuthResolved = true
|
|
}
|
|
}
|
|
|
|
func (m *SessionManager) releaseLogicalSession(key sessionKey, state *outboundState) {
|
|
if state != nil {
|
|
state.releaseAll()
|
|
}
|
|
m.mu.RLock()
|
|
hook := m.logicalSessionReleased
|
|
m.mu.RUnlock()
|
|
if hook != nil {
|
|
hook(key)
|
|
}
|
|
}
|
|
|
|
// ForgetLogicalSessionsForRawAuthKey is the terminal auth-key destruction path.
|
|
// Once destroy_auth_key_ok is on the wire the key can never reconnect to ACK or
|
|
// replay an old answer, so retaining any session payload or receipt is useless.
|
|
func (m *SessionManager) ForgetLogicalSessionsForRawAuthKey(authKeyID [8]byte) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
var release []*logicalSession
|
|
m.mu.Lock()
|
|
for key, logical := range m.logicalSessions {
|
|
if key.authKeyID != authKeyID {
|
|
continue
|
|
}
|
|
delete(m.logicalSessions, key)
|
|
if logical != nil {
|
|
release = append(release, logical)
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
for _, logical := range release {
|
|
m.releaseLogicalSession(logical.key, logical.outbound)
|
|
}
|
|
}
|
|
|
|
func (m *SessionManager) sweepLogicalSessions(now time.Time) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
var release []*logicalSession
|
|
m.mu.Lock()
|
|
for key, logical := range m.logicalSessions {
|
|
if logical == nil || logical.offlineAt.IsZero() || now.Sub(logical.offlineAt) < logicalSessionOfflineTTL {
|
|
continue
|
|
}
|
|
if m.bySession[key] != nil || m.claims[key] != nil {
|
|
logical.offlineAt = time.Time{}
|
|
continue
|
|
}
|
|
delete(m.logicalSessions, key)
|
|
release = append(release, logical)
|
|
}
|
|
m.mu.Unlock()
|
|
for _, logical := range release {
|
|
m.releaseLogicalSession(logical.key, logical.outbound)
|
|
}
|
|
}
|
|
|
|
func (m *SessionManager) releaseAllLogicalSessions() {
|
|
if m == nil {
|
|
return
|
|
}
|
|
var release []*logicalSession
|
|
m.mu.Lock()
|
|
for key, logical := range m.logicalSessions {
|
|
delete(m.logicalSessions, key)
|
|
if logical != nil {
|
|
release = append(release, logical)
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
for _, logical := range release {
|
|
m.releaseLogicalSession(logical.key, logical.outbound)
|
|
}
|
|
}
|
|
|
|
// rpcResult returns the exact unacknowledged wire result owned by the logical
|
|
// session. The receipt ledger calls this only after validating request identity.
|
|
func (m *SessionManager) rpcResult(authKeyID [8]byte, sessionID, reqMsgID int64) (*encodedOutboundMessage, bool) {
|
|
if m == nil || reqMsgID == 0 {
|
|
return nil, false
|
|
}
|
|
key := sessionKey{authKeyID: authKeyID, sessionID: sessionID}
|
|
m.mu.RLock()
|
|
logical := m.logicalSessions[key]
|
|
m.mu.RUnlock()
|
|
if logical == nil || logical.outbound == nil {
|
|
return nil, false
|
|
}
|
|
return logical.outbound.rpcResult(reqMsgID)
|
|
}
|