toc_conn.go 9.9 KB

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