toc_conn.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. package toc
  2. import (
  3. "net"
  4. "slices"
  5. "sync"
  6. "time"
  7. "golang.org/x/time/rate"
  8. "github.com/mk6i/open-oscar-server/state"
  9. "github.com/mk6i/open-oscar-server/wire"
  10. )
  11. // tocConn holds the state of one TOC client connection: its connections to
  12. // OSCAR services, the chat rooms it's in, its in-flight SNAC requests, and the
  13. // TOC protocol version it speaks.
  14. type tocConn struct {
  15. // bos is the connection to the BOS service.
  16. bos oscarConn
  17. // chats tracks the chat rooms the user has been invited to or joined.
  18. chats *ChatRegistry
  19. // cache matches SNAC replies to the requests that produced them.
  20. cache *snacCache
  21. // ident is the user's screen name.
  22. ident state.IdentScreenName
  23. // isTOC2 is true for TOC2 clients.
  24. isTOC2 bool
  25. // supportsMsgEnc is true for TOC2 clients that signed on with toc2_login.
  26. supportsMsgEnc bool
  27. // toCh queues TOC responses for the client.
  28. toCh chan<- []string
  29. // goAsync runs f for the lifetime of the TOC connection.
  30. goAsync func(f func() error)
  31. // clientAddr is the TOC client's address, which the OSCAR server sees as
  32. // the user's address.
  33. clientAddr net.Addr
  34. // limiter rate limits the commands that TOC handles without the OSCAR
  35. // server. The OSCAR server rate limits everything else.
  36. limiter *rate.Limiter
  37. // backlog holds BOS SNACs that arrived during signon, before RecvBOS
  38. // started.
  39. backlog []wire.SNACMessage
  40. // feedbag is the TOC2 user's buddy list.
  41. feedbag *feedbagMirror
  42. }
  43. // newTOCConn creates a TOC connection for the signed-on user ident whose BOS
  44. // connection is bos.
  45. func newTOCConn(bos oscarConn, ident state.IdentScreenName, clientAddr net.Addr) *tocConn {
  46. return &tocConn{
  47. bos: bos,
  48. chats: NewChatRegistry(),
  49. cache: newSNACCache(),
  50. ident: ident,
  51. clientAddr: clientAddr,
  52. limiter: rate.NewLimiter(rate.Every(1500*time.Millisecond), 5),
  53. feedbag: &feedbagMirror{},
  54. }
  55. }
  56. // IdentScreenName returns the user's screen name.
  57. func (c *tocConn) IdentScreenName() state.IdentScreenName {
  58. return c.ident
  59. }
  60. // IsTOC2 returns true when the client speaks TOC2, with or without encoded
  61. // messaging.
  62. func (c *tocConn) IsTOC2() bool {
  63. return c.isTOC2
  64. }
  65. // SupportsTOC2MsgEnc returns true when the client signed on with toc2_login,
  66. // which supports encoded messaging.
  67. func (c *tocConn) SupportsTOC2MsgEnc() bool {
  68. return c.supportsMsgEnc
  69. }
  70. // NewChatRegistry creates a new ChatRegistry instance.
  71. func NewChatRegistry() *ChatRegistry {
  72. return &ChatRegistry{
  73. lookup: make(map[int]wire.ICBMRoomInfo),
  74. conns: make(map[int]oscarConn),
  75. }
  76. }
  77. // ChatRegistry manages the chat rooms that a user is connected to during a TOC
  78. // session. It maps chat room identifiers to room metadata and to the
  79. // connection for each joined room.
  80. //
  81. // This struct provides thread-safe operations for adding, retrieving, and
  82. // managing chat room metadata and associated connections.
  83. type ChatRegistry struct {
  84. lookup map[int]wire.ICBMRoomInfo // Maps chat room IDs to their metadata.
  85. conns map[int]oscarConn // Tracks chat room connections by chat room ID.
  86. nextID int // Incremental identifier for newly added chat rooms.
  87. m sync.RWMutex // Synchronization primitive for concurrent access.
  88. }
  89. // Add registers metadata for a newly joined chat room and returns a unique
  90. // identifier for it. If the room is already registered, it returns the existing ID.
  91. func (c *ChatRegistry) Add(room wire.ICBMRoomInfo) int {
  92. c.m.Lock()
  93. defer c.m.Unlock()
  94. for chatID, r := range c.lookup {
  95. if r == room {
  96. return chatID
  97. }
  98. }
  99. id := c.nextID
  100. c.lookup[id] = room
  101. c.nextID++
  102. return id
  103. }
  104. // LookupRoom retrieves metadata for the chat room registered with chatID.
  105. // It returns the room metadata and a boolean indicating whether the chat ID
  106. // was found.
  107. func (c *ChatRegistry) LookupRoom(chatID int) (wire.ICBMRoomInfo, bool) {
  108. c.m.RLock()
  109. defer c.m.RUnlock()
  110. room, found := c.lookup[chatID]
  111. return room, found
  112. }
  113. // RegisterConn associates a chat room connection with a chat ID. If a
  114. // connection is already registered for the given chat ID, it will be
  115. // overwritten.
  116. func (c *ChatRegistry) RegisterConn(chatID int, conn oscarConn) {
  117. c.m.Lock()
  118. defer c.m.Unlock()
  119. c.conns[chatID] = conn
  120. }
  121. // RetrieveConn retrieves the chat room connection associated with the given
  122. // chat ID. If no connection is registered for the chat ID, it returns nil.
  123. func (c *ChatRegistry) RetrieveConn(chatID int) oscarConn {
  124. c.m.RLock()
  125. defer c.m.RUnlock()
  126. return c.conns[chatID]
  127. }
  128. // RemoveConn removes a chat room connection.
  129. func (c *ChatRegistry) RemoveConn(chatID int) {
  130. c.m.Lock()
  131. defer c.m.Unlock()
  132. delete(c.conns, chatID)
  133. }
  134. // feedbagMirror is TOC's copy of the user's feedbag (server-side buddy list).
  135. // Iit holds the feedbag as the server last confirmed it plus the changes TOC
  136. // sent that the server hasn't confirmed yet. Commands work from the
  137. // feedbag as it will be once those changes land, and only confirmed changes
  138. // become part of the confirmed feedbag.
  139. type feedbagMirror struct {
  140. // sendMu keeps changes and reloads in the order they're sent.
  141. sendMu sync.Mutex
  142. mu sync.Mutex
  143. confirmed []wire.FeedbagItem
  144. pending []feedbagChange
  145. lastSeq uint64
  146. }
  147. // feedbagChange is a feedbag change sent to the server and not yet
  148. // confirmed.
  149. type feedbagChange struct {
  150. seq uint64
  151. requestID uint32
  152. del bool
  153. items []wire.FeedbagItem
  154. }
  155. // snapshot returns the feedbag with all pending changes applied.
  156. func (m *feedbagMirror) snapshot() []wire.FeedbagItem {
  157. m.mu.Lock()
  158. defer m.mu.Unlock()
  159. items := append([]wire.FeedbagItem(nil), m.confirmed...)
  160. for _, change := range m.pending {
  161. items = applyFeedbagChange(items, change.del, change.items)
  162. }
  163. return cloneFeedbagItems(items)
  164. }
  165. // reset replaces the confirmed feedbag with items and forgets pending
  166. // changes.
  167. func (m *feedbagMirror) reset(items []wire.FeedbagItem) {
  168. m.mu.Lock()
  169. defer m.mu.Unlock()
  170. m.confirmed = cloneFeedbagItems(items)
  171. m.pending = nil
  172. }
  173. // sent records a change TOC is sending as request requestID. del is true
  174. // when the change deletes items.
  175. func (m *feedbagMirror) sent(requestID uint32, del bool, items []wire.FeedbagItem) {
  176. m.mu.Lock()
  177. defer m.mu.Unlock()
  178. m.lastSeq++
  179. m.pending = append(m.pending, feedbagChange{
  180. seq: m.lastSeq,
  181. requestID: requestID,
  182. del: del,
  183. items: cloneFeedbagItems(items),
  184. })
  185. }
  186. // confirm applies the items of change requestID that the server accepted,
  187. // according to results, and forgets the change.
  188. func (m *feedbagMirror) confirm(requestID uint32, results []uint16) {
  189. m.mu.Lock()
  190. defer m.mu.Unlock()
  191. i := m.indexOf(requestID)
  192. if i < 0 {
  193. return
  194. }
  195. change := m.pending[i]
  196. var accepted []wire.FeedbagItem
  197. for j, item := range change.items {
  198. if j < len(results) && results[j] == 0 {
  199. accepted = append(accepted, item)
  200. }
  201. }
  202. m.confirmed = applyFeedbagChange(m.confirmed, change.del, accepted)
  203. m.pending = append(m.pending[:i], m.pending[i+1:]...)
  204. }
  205. // discard forgets change requestID, which the server rejected.
  206. func (m *feedbagMirror) discard(requestID uint32) {
  207. m.mu.Lock()
  208. defer m.mu.Unlock()
  209. if i := m.indexOf(requestID); i >= 0 {
  210. m.pending = append(m.pending[:i], m.pending[i+1:]...)
  211. }
  212. }
  213. // hasPending returns true if any change is awaiting confirmation.
  214. func (m *feedbagMirror) hasPending() bool {
  215. m.mu.Lock()
  216. defer m.mu.Unlock()
  217. return len(m.pending) > 0
  218. }
  219. // mark returns a marker for the changes sent so far, for reload.
  220. func (m *feedbagMirror) mark() uint64 {
  221. m.mu.Lock()
  222. defer m.mu.Unlock()
  223. return m.lastSeq
  224. }
  225. // reload replaces the confirmed feedbag with items, the server's feedbag as of
  226. // a query sent after the changes up to marker. Those changes are forgotten,
  227. // since the server either applied them or dropped them. Changes sent after the
  228. // query stay pending.
  229. func (m *feedbagMirror) reload(marker uint64, items []wire.FeedbagItem) {
  230. m.mu.Lock()
  231. defer m.mu.Unlock()
  232. m.confirmed = cloneFeedbagItems(items)
  233. var pending []feedbagChange
  234. for _, change := range m.pending {
  235. if change.seq > marker {
  236. pending = append(pending, change)
  237. }
  238. }
  239. m.pending = pending
  240. }
  241. // applyRemote applies a change made at another of the user's locations. del
  242. // is true when the change deletes items.
  243. func (m *feedbagMirror) applyRemote(del bool, items []wire.FeedbagItem) {
  244. m.mu.Lock()
  245. defer m.mu.Unlock()
  246. m.confirmed = applyFeedbagChange(m.confirmed, del, cloneFeedbagItems(items))
  247. }
  248. // groupNames returns the name of each group by group ID, with all pending
  249. // changes applied.
  250. func (m *feedbagMirror) groupNames() map[uint16]string {
  251. names := make(map[uint16]string)
  252. for _, item := range m.snapshot() {
  253. if item.ClassID == wire.FeedbagClassIdGroup {
  254. names[item.GroupID] = item.Name
  255. }
  256. }
  257. return names
  258. }
  259. func (m *feedbagMirror) indexOf(requestID uint32) int {
  260. for i, change := range m.pending {
  261. if change.requestID == requestID {
  262. return i
  263. }
  264. }
  265. return -1
  266. }
  267. // cloneFeedbagItems returns a copy of items that shares no TLV lists with
  268. // them, since state.FeedbagList edits TLV lists in place.
  269. func cloneFeedbagItems(items []wire.FeedbagItem) []wire.FeedbagItem {
  270. items = slices.Clone(items)
  271. for i := range items {
  272. items[i].TLVList = slices.Clone(items[i].TLVList)
  273. }
  274. return items
  275. }
  276. // applyFeedbagChange returns items with changed upserted, or deleted when del
  277. // is true. Items match by group and item ID.
  278. func applyFeedbagChange(items []wire.FeedbagItem, del bool, changed []wire.FeedbagItem) []wire.FeedbagItem {
  279. for _, item := range changed {
  280. i := slices.IndexFunc(items, func(existing wire.FeedbagItem) bool {
  281. return existing.GroupID == item.GroupID && existing.ItemID == item.ItemID
  282. })
  283. switch {
  284. case del && i >= 0:
  285. items = slices.Delete(items, i, i+1)
  286. case !del && i >= 0:
  287. items[i] = item
  288. case !del:
  289. items = append(items, item)
  290. }
  291. }
  292. return items
  293. }