types.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. package icq_legacy
  2. import (
  3. "context"
  4. "log/slog"
  5. "net"
  6. "sync"
  7. "time"
  8. "github.com/mk6i/open-oscar-server/state"
  9. "github.com/mk6i/open-oscar-server/wire"
  10. )
  11. // truncateField truncates a string field to maxLen if it exceeds the limit,
  12. // logging a warning when truncation occurs. This prevents oversized data from
  13. // crashing old ICQ clients that have fixed-size buffers.
  14. func truncateField(value string, maxLen int, logger *slog.Logger, fieldName string, uin uint32) string {
  15. if len(value) <= maxLen {
  16. return value
  17. }
  18. logger.Warn("truncating oversized field for legacy client",
  19. "uin", uin,
  20. "field", fieldName,
  21. "max", maxLen,
  22. "got", len(value),
  23. )
  24. return value[:maxLen]
  25. }
  26. // LegacySession represents a connected legacy ICQ client session.
  27. // It holds all state for a single UDP-based ICQ connection, including
  28. // protocol version, sequence numbers, contact/visibility lists, and
  29. // direct connection info for peer-to-peer messaging.
  30. type LegacySession struct {
  31. // UIN is the user's ICQ identification number.
  32. UIN uint32
  33. // Addr is the UDP address of the connected client.
  34. Addr *net.UDPAddr
  35. // Version is the ICQ protocol version (2, 3, 4, or 5).
  36. Version uint16
  37. // SessionID is the V5 session identifier used for packet encryption.
  38. SessionID uint32 // V5 only
  39. // SeqNumClient is the last received sequence number from the client.
  40. SeqNumClient uint16
  41. // SeqNumServer is the next sequence number to send to the client.
  42. SeqNumServer uint16
  43. // Status is the user's current online status (online, away, DND, etc.).
  44. Status uint32
  45. // LastActivity is the timestamp of the last received packet from this session.
  46. LastActivity time.Time
  47. // ContactList is the user's buddy list (UINs of contacts).
  48. ContactList []uint32
  49. // VisibleList contains UINs that can see this user when invisible.
  50. VisibleList []uint32
  51. // InvisibleList contains UINs that cannot see this user's online status.
  52. InvisibleList []uint32
  53. // Password is stored for session validation during the connection lifetime.
  54. Password string
  55. // Direct connection info (for peer-to-peer messaging)
  56. // TCPPort is the client's TCP listening port for direct connections.
  57. TCPPort uint32
  58. // InternalIP is the client's internal/LAN IP address for direct connections.
  59. InternalIP uint32
  60. // DCVersion is the client's direct connection protocol version.
  61. DCVersion uint16
  62. // DCType is the direct connection type (normal, SOCKS, etc.).
  63. DCType uint8
  64. // Instance links this legacy session to the unified OSCAR session manager.
  65. Instance *state.SessionInstance
  66. // knownOnline tracks contacts that have been reported as online via
  67. // SendUserOnline. Subsequent BuddyArrived for the same contact use
  68. // SendStatusChange instead, which sends the correct V5 packet type.
  69. knownOnline map[uint32]bool
  70. mu sync.RWMutex
  71. }
  72. // UpdateActivity updates the last activity timestamp
  73. func (s *LegacySession) UpdateActivity() {
  74. s.mu.Lock()
  75. defer s.mu.Unlock()
  76. s.LastActivity = time.Now()
  77. }
  78. // GetLastActivity returns the last activity timestamp
  79. func (s *LegacySession) GetLastActivity() time.Time {
  80. s.mu.RLock()
  81. defer s.mu.RUnlock()
  82. return s.LastActivity
  83. }
  84. // NextServerSeqNum returns and increments the server sequence number
  85. func (s *LegacySession) NextServerSeqNum() uint16 {
  86. s.mu.Lock()
  87. defer s.mu.Unlock()
  88. seq := s.SeqNumServer
  89. s.SeqNumServer++
  90. return seq
  91. }
  92. // SetStatus updates the session status
  93. func (s *LegacySession) SetStatus(status uint32) {
  94. s.mu.Lock()
  95. defer s.mu.Unlock()
  96. s.Status = status
  97. }
  98. // GetStatus returns the current status
  99. func (s *LegacySession) GetStatus() uint32 {
  100. s.mu.RLock()
  101. defer s.mu.RUnlock()
  102. return s.Status
  103. }
  104. // SetContactList updates the contact list
  105. func (s *LegacySession) SetContactList(contacts []uint32) {
  106. s.mu.Lock()
  107. defer s.mu.Unlock()
  108. s.ContactList = make([]uint32, len(contacts))
  109. copy(s.ContactList, contacts)
  110. }
  111. // GetContactList returns a copy of the contact list
  112. func (s *LegacySession) GetContactList() []uint32 {
  113. s.mu.RLock()
  114. defer s.mu.RUnlock()
  115. contacts := make([]uint32, len(s.ContactList))
  116. copy(contacts, s.ContactList)
  117. return contacts
  118. }
  119. // SetVisibleList updates the visible list with a copy of the provided UINs.
  120. // Users on the visible list can see this session's online status even when invisible.
  121. func (s *LegacySession) SetVisibleList(visible []uint32) {
  122. s.mu.Lock()
  123. defer s.mu.Unlock()
  124. s.VisibleList = make([]uint32, len(visible))
  125. copy(s.VisibleList, visible)
  126. }
  127. // SetInvisibleList updates the invisible list with a copy of the provided UINs.
  128. // Users on the invisible list cannot see this session's online status.
  129. func (s *LegacySession) SetInvisibleList(invisible []uint32) {
  130. s.mu.Lock()
  131. defer s.mu.Unlock()
  132. s.InvisibleList = make([]uint32, len(invisible))
  133. copy(s.InvisibleList, invisible)
  134. }
  135. // IsOnVisibleList checks if a UIN is on the visible list
  136. func (s *LegacySession) IsOnVisibleList(uin uint32) bool {
  137. s.mu.RLock()
  138. defer s.mu.RUnlock()
  139. for _, v := range s.VisibleList {
  140. if v == uin {
  141. return true
  142. }
  143. }
  144. return false
  145. }
  146. // IsOnInvisibleList checks if a UIN is on the invisible list
  147. func (s *LegacySession) IsOnInvisibleList(uin uint32) bool {
  148. s.mu.RLock()
  149. defer s.mu.RUnlock()
  150. for _, v := range s.InvisibleList {
  151. if v == uin {
  152. return true
  153. }
  154. }
  155. return false
  156. }
  157. // IsContact checks if a UIN is in the contact list
  158. func (s *LegacySession) IsContact(uin uint32) bool {
  159. s.mu.RLock()
  160. defer s.mu.RUnlock()
  161. for _, c := range s.ContactList {
  162. if c == uin {
  163. return true
  164. }
  165. }
  166. return false
  167. }
  168. // MarkContactOnline marks a contact as known-online. Returns true if the
  169. // contact was already known online (i.e. this is a status change, not an
  170. // initial arrival).
  171. func (s *LegacySession) MarkContactOnline(uin uint32) bool {
  172. s.mu.Lock()
  173. defer s.mu.Unlock()
  174. if s.knownOnline == nil {
  175. s.knownOnline = make(map[uint32]bool)
  176. }
  177. wasOnline := s.knownOnline[uin]
  178. s.knownOnline[uin] = true
  179. return wasOnline
  180. }
  181. // MarkContactOffline removes a contact from the known-online set.
  182. func (s *LegacySession) MarkContactOffline(uin uint32) {
  183. s.mu.Lock()
  184. defer s.mu.Unlock()
  185. if s.knownOnline != nil {
  186. delete(s.knownOnline, uin)
  187. }
  188. }
  189. // GetUIN returns the session's UIN (implements LegacySessionInstance)
  190. func (s *LegacySession) GetUIN() uint32 {
  191. return s.UIN
  192. }
  193. // GetExternalIP returns the external IP as uint32 (from UDP address)
  194. func (s *LegacySession) GetExternalIP() uint32 {
  195. if s.Addr == nil || s.Addr.IP == nil {
  196. return 0
  197. }
  198. ip := s.Addr.IP.To4()
  199. if ip == nil {
  200. return 0
  201. }
  202. // ICQ stores IP in little-endian format
  203. return uint32(ip[0]) | uint32(ip[1])<<8 | uint32(ip[2])<<16 | uint32(ip[3])<<24
  204. }
  205. // GetTCPPort returns the TCP port for direct connections
  206. func (s *LegacySession) GetTCPPort() uint32 {
  207. s.mu.RLock()
  208. defer s.mu.RUnlock()
  209. return s.TCPPort
  210. }
  211. // GetInternalIP returns the internal IP for direct connections
  212. func (s *LegacySession) GetInternalIP() uint32 {
  213. s.mu.RLock()
  214. defer s.mu.RUnlock()
  215. return s.InternalIP
  216. }
  217. // GetDCVersion returns the direct connection protocol version
  218. func (s *LegacySession) GetDCVersion() uint16 {
  219. s.mu.RLock()
  220. defer s.mu.RUnlock()
  221. return s.DCVersion
  222. }
  223. // SetDirectConnectionInfo sets the direct connection parameters
  224. func (s *LegacySession) SetDirectConnectionInfo(tcpPort, internalIP uint32, dcVersion uint16, dcType uint8) {
  225. s.mu.Lock()
  226. defer s.mu.Unlock()
  227. s.TCPPort = tcpPort
  228. s.InternalIP = internalIP
  229. s.DCVersion = dcVersion
  230. s.DCType = dcType
  231. }
  232. // UserManager provides user lookup and management for legacy ICQ operations.
  233. type UserManager interface {
  234. User(ctx context.Context, screenName state.IdentScreenName) (*state.User, error)
  235. InsertUser(ctx context.Context, u state.User) error
  236. DeleteUser(ctx context.Context, screenName state.IdentScreenName) error
  237. }
  238. // AccountManager provides password management for legacy ICQ operations.
  239. type AccountManager interface {
  240. SetUserPassword(ctx context.Context, screenName state.IdentScreenName, newPassword string) error
  241. }
  242. // SessionRetriever provides OSCAR session lookup for cross-protocol routing.
  243. type SessionRetriever interface {
  244. RetrieveSession(screenName state.IdentScreenName) *state.Session
  245. }
  246. // MessageRelayer provides SNAC message delivery for legacy→OSCAR routing.
  247. type MessageRelayer interface {
  248. RelayToScreenName(ctx context.Context, screenName state.IdentScreenName, msg wire.SNACMessage)
  249. }
  250. // BuddyBroadcaster provides presence broadcast for OSCAR buddy notifications.
  251. type BuddyBroadcaster interface {
  252. BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error
  253. BroadcastBuddyDeparted(ctx context.Context, screenName state.IdentScreenName) error
  254. }
  255. // OfflineMessageManager provides offline message storage and retrieval.
  256. type OfflineMessageManager interface {
  257. DeleteMessages(ctx context.Context, recip state.IdentScreenName) error
  258. RetrieveMessages(ctx context.Context, recip state.IdentScreenName) ([]state.OfflineMessage, error)
  259. SaveMessage(ctx context.Context, offlineMessage state.OfflineMessage) (int, error)
  260. }
  261. // ICQUserFinder provides user search capabilities for legacy ICQ operations.
  262. type ICQUserFinder interface {
  263. FindByUIN(ctx context.Context, UIN uint32) (state.User, error)
  264. FindByICQEmail(ctx context.Context, email string) (state.User, error)
  265. FindByICQName(ctx context.Context, firstName, lastName, nickName string) ([]state.User, error)
  266. FindByICQInterests(ctx context.Context, code uint16, keywords []string) ([]state.User, error)
  267. FindByICQKeyword(ctx context.Context, keyword string) ([]state.User, error)
  268. SearchICQUsers(ctx context.Context, c state.ICQUserSearchCriteria) ([]state.User, error)
  269. }
  270. // ICQUserUpdater provides profile update capabilities for legacy ICQ operations.
  271. type ICQUserUpdater interface {
  272. SetBasicInfo(ctx context.Context, name state.IdentScreenName, data state.ICQBasicInfo) error
  273. SetWorkInfo(ctx context.Context, name state.IdentScreenName, data state.ICQWorkInfo) error
  274. SetMoreInfo(ctx context.Context, name state.IdentScreenName, data state.ICQMoreInfo) error
  275. SetInterests(ctx context.Context, name state.IdentScreenName, data state.ICQInterests) error
  276. SetAffiliations(ctx context.Context, name state.IdentScreenName, data state.ICQAffiliations) error
  277. SetUserNotes(ctx context.Context, name state.IdentScreenName, data state.ICQUserNotes) error
  278. SetPermissions(ctx context.Context, name state.IdentScreenName, data state.ICQPermissions) error
  279. SetHomepageCategory(ctx context.Context, name state.IdentScreenName, data state.ICQHomepageCategory) error
  280. SetICQInfo(ctx context.Context, name state.IdentScreenName, info state.ICQInfo) error
  281. }
  282. // FeedbagManager provides server-side buddy list access and modification.
  283. type FeedbagManager interface {
  284. Feedbag(ctx context.Context, screenName state.IdentScreenName) ([]wire.FeedbagItem, error)
  285. FeedbagUpsert(ctx context.Context, screenName state.IdentScreenName, items []wire.FeedbagItem) error
  286. }
  287. // RelationshipFetcher provides buddy relationship lookup.
  288. type RelationshipFetcher interface {
  289. AllRelationships(ctx context.Context, me state.IdentScreenName, filter []state.IdentScreenName) ([]state.Relationship, error)
  290. Relationship(ctx context.Context, me state.IdentScreenName, them state.IdentScreenName) (state.Relationship, error)
  291. }
  292. // BuddyListRegistry provides buddy list registration for cross-protocol
  293. // presence visibility. Legacy sessions must register their buddy list so that
  294. // OSCAR's BroadcastVisibility/AllRelationships can discover them.
  295. type BuddyListRegistry interface {
  296. RegisterBuddyList(ctx context.Context, user state.IdentScreenName) error
  297. UnregisterBuddyList(ctx context.Context, user state.IdentScreenName) error
  298. }
  299. // BuddyService provides client-side buddy list management through the OSCAR
  300. // Buddy food group path used by OSCAR and TOC clients.
  301. type BuddyService interface {
  302. AddBuddies(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x03_0x04_BuddyAddBuddies) (*wire.SNACMessage, error)
  303. }
  304. // LegacySessionInstance represents a legacy session as seen by the service layer.
  305. // This interface abstracts the session to avoid circular dependencies between
  306. // the foodgroup and server/icq_legacy packages.
  307. type LegacySessionInstance interface {
  308. // GetUIN returns the session's ICQ identification number.
  309. GetUIN() uint32
  310. // GetStatus returns the session's current online status value.
  311. GetStatus() uint32
  312. // GetContactList returns a copy of the session's contact list (buddy UINs).
  313. GetContactList() []uint32
  314. // IsOnVisibleList checks if the given UIN is on this session's visible list.
  315. IsOnVisibleList(uin uint32) bool
  316. // IsOnInvisibleList checks if the given UIN is on this session's invisible list.
  317. IsOnInvisibleList(uin uint32) bool
  318. }
  319. // LegacyMessageSender is the interface for sending messages to legacy ICQ clients.
  320. // It provides methods for delivering messages and status notifications to
  321. // connected legacy sessions.
  322. type LegacyMessageSender interface {
  323. // SendMessage delivers a message to a legacy client identified by UIN.
  324. SendMessage(uin uint32, fromUIN uint32, msgType uint16, message string) error
  325. // SendStatusUpdate sends a status change notification to a legacy client.
  326. SendStatusUpdate(uin uint32, targetUIN uint32, status uint32) error
  327. // SendUserOnline sends a user online notification to a legacy client.
  328. SendUserOnline(uin uint32, targetUIN uint32, status uint32, ip net.IP, port uint16) error
  329. // SendUserOffline sends a user offline notification to a legacy client.
  330. SendUserOffline(uin uint32, targetUIN uint32) error
  331. }
  332. // ICBMService is the interface for sending SNAC messages to the OSCAR ICBM service.
  333. type ICBMService interface {
  334. ChannelMsgToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error)
  335. }