session.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package icq_legacy
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "net"
  6. "sync"
  7. "time"
  8. "github.com/mk6i/open-oscar-server/config"
  9. "github.com/mk6i/open-oscar-server/state"
  10. )
  11. // LegacySessionManager manages sessions for legacy ICQ clients
  12. type LegacySessionManager struct {
  13. sessions map[uint32]*LegacySession // Indexed by UIN
  14. addrIndex map[string]*LegacySession // Indexed by UDP address string
  15. sessionMgr SessionRegistry // Unified session manager
  16. bridge *LegacyMessageBridge // OSCAR->legacy bridge for message pump
  17. onSessionExpired func(session *LegacySession) // Called before removing expired sessions
  18. config config.ICQLegacyConfig
  19. logger *slog.Logger
  20. mu sync.RWMutex
  21. }
  22. // SessionRegistry is the interface for the unified session manager
  23. type SessionRegistry interface {
  24. RemoveSession(session *state.Session)
  25. }
  26. // NewLegacySessionManager creates a new session manager
  27. func NewLegacySessionManager(sessionMgr SessionRegistry, cfg config.ICQLegacyConfig, logger *slog.Logger) *LegacySessionManager {
  28. return &LegacySessionManager{
  29. sessions: make(map[uint32]*LegacySession),
  30. addrIndex: make(map[string]*LegacySession),
  31. sessionMgr: sessionMgr,
  32. config: cfg,
  33. logger: logger,
  34. }
  35. }
  36. // SetBridge sets the OSCAR->legacy bridge used to start message pumps for new
  37. // sessions. Must be called before any sessions are created.
  38. func (m *LegacySessionManager) SetBridge(bridge *LegacyMessageBridge) {
  39. m.bridge = bridge
  40. }
  41. // SetOnSessionExpired sets a callback invoked before an expired session is
  42. // removed. The callback should notify contacts and OSCAR clients that the
  43. // user went offline — mirroring what handleLogoff does for graceful logouts.
  44. func (m *LegacySessionManager) SetOnSessionExpired(fn func(session *LegacySession)) {
  45. m.onSessionExpired = fn
  46. }
  47. // CreateSession creates a new legacy session backed by an OSCAR session instance.
  48. func (m *LegacySessionManager) CreateSession(uin uint32, addr *net.UDPAddr, version uint16, instance *state.SessionInstance) (*LegacySession, error) {
  49. if instance == nil {
  50. return nil, fmt.Errorf("missing OSCAR session instance for UIN %d", uin)
  51. }
  52. m.mu.Lock()
  53. defer m.mu.Unlock()
  54. // Check if session already exists
  55. if existing, ok := m.sessions[uin]; ok {
  56. // Remove old session
  57. m.removeSessionLocked(existing)
  58. }
  59. session := newLegacySession(uin, addr, version, instance)
  60. m.activateSessionLocked(session)
  61. m.sessions[uin] = session
  62. m.addrIndex[addr.String()] = session
  63. m.logger.Info("created legacy session",
  64. "uin", uin,
  65. "addr", addr.String(),
  66. "version", version,
  67. "session_id", session.SessionID,
  68. )
  69. return session, nil
  70. }
  71. // CreatePendingSession creates a legacy session before the OSCAR session exists.
  72. func (m *LegacySessionManager) CreatePendingSession(uin uint32, addr *net.UDPAddr, version uint16) (*LegacySession, error) {
  73. m.mu.Lock()
  74. defer m.mu.Unlock()
  75. if existing, ok := m.sessions[uin]; ok {
  76. m.removeSessionLocked(existing)
  77. }
  78. session := newLegacySession(uin, addr, version, nil)
  79. m.sessions[uin] = session
  80. m.addrIndex[addr.String()] = session
  81. m.logger.Info("created pending legacy session",
  82. "uin", uin,
  83. "addr", addr.String(),
  84. "version", version,
  85. "session_id", session.SessionID,
  86. )
  87. return session, nil
  88. }
  89. // AttachOSCARSession attaches an AuthService-created OSCAR instance to a pending legacy session.
  90. func (m *LegacySessionManager) AttachOSCARSession(uin uint32, addr *net.UDPAddr, instance *state.SessionInstance) (*LegacySession, error) {
  91. if instance == nil {
  92. return nil, fmt.Errorf("missing OSCAR session instance for UIN %d", uin)
  93. }
  94. m.mu.Lock()
  95. defer m.mu.Unlock()
  96. session, ok := m.sessions[uin]
  97. if !ok {
  98. return nil, fmt.Errorf("pending legacy session not found for UIN %d", uin)
  99. }
  100. if session.Instance != nil && session.Instance != instance {
  101. return nil, fmt.Errorf("legacy session for UIN %d already has an OSCAR session", uin)
  102. }
  103. if session.Addr != nil {
  104. delete(m.addrIndex, session.Addr.String())
  105. }
  106. session.Addr = addr
  107. m.addrIndex[addr.String()] = session
  108. session.Instance = instance
  109. m.activateSessionLocked(session)
  110. m.logger.Info("attached OSCAR session to legacy session",
  111. "uin", uin,
  112. "addr", addr.String(),
  113. "version", session.Version,
  114. "session_id", session.SessionID,
  115. )
  116. return session, nil
  117. }
  118. func newLegacySession(uin uint32, addr *net.UDPAddr, version uint16, instance *state.SessionInstance) *LegacySession {
  119. return &LegacySession{
  120. UIN: uin,
  121. Addr: addr,
  122. Version: version,
  123. SessionID: GenerateSessionID(),
  124. SeqNumServer: 0, // V2 spec: server starts counting at 0
  125. Status: ICQLegacyStatusOnline,
  126. LastActivity: time.Now(),
  127. Instance: instance,
  128. }
  129. }
  130. func (m *LegacySessionManager) activateSessionLocked(session *LegacySession) {
  131. session.Instance.SetSignonComplete()
  132. // Start the OSCAR message pump for this session. This goroutine drains
  133. // SNAC messages (BuddyArrived/BuddyDeparted) that the OSCAR buddy system
  134. // sends to this session's unified SessionInstance, and converts them into
  135. // legacy protocol packets. Without this, OSCAR->legacy status notifications
  136. // would be silently dropped.
  137. if m.bridge != nil {
  138. m.bridge.StartOSCARMessagePump(session)
  139. }
  140. }
  141. // GetSession retrieves a session by UIN
  142. func (m *LegacySessionManager) GetSession(uin uint32) *LegacySession {
  143. m.mu.RLock()
  144. defer m.mu.RUnlock()
  145. return m.sessions[uin]
  146. }
  147. // GetSessionByAddr retrieves a session by UDP address
  148. func (m *LegacySessionManager) GetSessionByAddr(addr *net.UDPAddr) *LegacySession {
  149. m.mu.RLock()
  150. defer m.mu.RUnlock()
  151. return m.addrIndex[addr.String()]
  152. }
  153. // RemoveSession removes a session by UIN
  154. func (m *LegacySessionManager) RemoveSession(uin uint32) {
  155. m.mu.Lock()
  156. defer m.mu.Unlock()
  157. if session, ok := m.sessions[uin]; ok {
  158. m.removeSessionLocked(session)
  159. }
  160. }
  161. // removeSessionLocked removes a session (must be called with lock held)
  162. func (m *LegacySessionManager) removeSessionLocked(session *LegacySession) {
  163. // Remove from unified session manager
  164. if session.Instance != nil {
  165. m.sessionMgr.RemoveSession(session.Instance.Session())
  166. }
  167. // Remove from indexes
  168. delete(m.sessions, session.UIN)
  169. if session.Addr != nil {
  170. delete(m.addrIndex, session.Addr.String())
  171. }
  172. m.logger.Info("removed legacy session",
  173. "uin", session.UIN,
  174. )
  175. }
  176. // UpdateSessionAddr updates the UDP address for a session
  177. // This handles clients that reconnect from a different address
  178. func (m *LegacySessionManager) UpdateSessionAddr(uin uint32, newAddr *net.UDPAddr) {
  179. m.mu.Lock()
  180. defer m.mu.Unlock()
  181. session, ok := m.sessions[uin]
  182. if !ok {
  183. return
  184. }
  185. // Remove old address index
  186. if session.Addr != nil {
  187. delete(m.addrIndex, session.Addr.String())
  188. }
  189. // Update address
  190. session.Addr = newAddr
  191. m.addrIndex[newAddr.String()] = session
  192. }
  193. // CleanupExpired removes sessions that have timed out
  194. func (m *LegacySessionManager) CleanupExpired() int {
  195. m.mu.Lock()
  196. timeout := m.config.SessionTimeout
  197. now := time.Now()
  198. // Collect expired sessions while holding the lock
  199. var expired []*LegacySession
  200. for _, session := range m.sessions {
  201. if now.Sub(session.GetLastActivity()) > timeout {
  202. expired = append(expired, session)
  203. }
  204. }
  205. m.mu.Unlock()
  206. // Notify contacts outside the lock to avoid deadlock
  207. for _, session := range expired {
  208. m.logger.Info("cleaning up expired session",
  209. "uin", session.UIN,
  210. "last_activity", session.GetLastActivity(),
  211. )
  212. if m.onSessionExpired != nil {
  213. m.onSessionExpired(session)
  214. }
  215. }
  216. // Re-acquire lock and remove the sessions
  217. m.mu.Lock()
  218. defer m.mu.Unlock()
  219. removed := 0
  220. for _, session := range expired {
  221. if _, ok := m.sessions[session.UIN]; ok {
  222. m.removeSessionLocked(session)
  223. removed++
  224. }
  225. }
  226. return removed
  227. }
  228. // GetAllSessions returns all active sessions
  229. func (m *LegacySessionManager) GetAllSessions() []*LegacySession {
  230. m.mu.RLock()
  231. defer m.mu.RUnlock()
  232. sessions := make([]*LegacySession, 0, len(m.sessions))
  233. for _, session := range m.sessions {
  234. sessions = append(sessions, session)
  235. }
  236. return sessions
  237. }
  238. // GetOnlineContacts returns the online contacts for a session
  239. func (m *LegacySessionManager) GetOnlineContacts(session *LegacySession) []*LegacySession {
  240. m.mu.RLock()
  241. defer m.mu.RUnlock()
  242. contacts := session.GetContactList()
  243. online := make([]*LegacySession, 0)
  244. for _, uin := range contacts {
  245. if contactSession, ok := m.sessions[uin]; ok {
  246. // Check visibility rules
  247. if !m.isVisibleTo(contactSession, session.UIN) {
  248. continue
  249. }
  250. online = append(online, contactSession)
  251. }
  252. }
  253. return online
  254. }
  255. // isVisibleTo checks if a session is visible to a specific UIN
  256. func (m *LegacySessionManager) isVisibleTo(session *LegacySession, viewerUIN uint32) bool {
  257. status := session.GetStatus()
  258. // If invisible, only visible to those on visible list
  259. if status&ICQLegacyStatusInvisible != 0 {
  260. return session.IsOnVisibleList(viewerUIN)
  261. }
  262. // If on invisible list, not visible
  263. if session.IsOnInvisibleList(viewerUIN) {
  264. return false
  265. }
  266. return true
  267. }
  268. // BroadcastToContacts sends a notification to all contacts of a session
  269. func (m *LegacySessionManager) BroadcastToContacts(session *LegacySession, fn func(*LegacySession)) {
  270. m.mu.RLock()
  271. defer m.mu.RUnlock()
  272. contacts := session.GetContactList()
  273. for _, uin := range contacts {
  274. if contactSession, ok := m.sessions[uin]; ok {
  275. fn(contactSession)
  276. }
  277. }
  278. }
  279. // NotifyContactsOfStatus notifies contacts of a status change
  280. func (m *LegacySessionManager) NotifyContactsOfStatus(session *LegacySession) []uint32 {
  281. m.mu.RLock()
  282. defer m.mu.RUnlock()
  283. // Find all sessions that have this user in their contact list
  284. notified := make([]uint32, 0)
  285. for uin, otherSession := range m.sessions {
  286. if uin == session.UIN {
  287. continue
  288. }
  289. if otherSession.IsContact(session.UIN) {
  290. // Check visibility
  291. if m.isVisibleTo(session, uin) {
  292. notified = append(notified, uin)
  293. }
  294. }
  295. }
  296. return notified
  297. }
  298. // Count returns the number of active sessions
  299. func (m *LegacySessionManager) Count() int {
  300. m.mu.RLock()
  301. defer m.mu.RUnlock()
  302. return len(m.sessions)
  303. }
  304. // StartCleanupRoutine starts a goroutine that periodically cleans up expired sessions
  305. func (m *LegacySessionManager) StartCleanupRoutine(interval time.Duration, stop <-chan struct{}) {
  306. ticker := time.NewTicker(interval)
  307. defer ticker.Stop()
  308. for {
  309. select {
  310. case <-ticker.C:
  311. removed := m.CleanupExpired()
  312. if removed > 0 {
  313. m.logger.Info("session cleanup completed",
  314. "removed", removed,
  315. "remaining", m.Count(),
  316. )
  317. }
  318. case <-stop:
  319. return
  320. }
  321. }
  322. }