oservice.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  1. package foodgroup
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "time"
  9. "github.com/mk6i/open-oscar-server/config"
  10. "github.com/mk6i/open-oscar-server/state"
  11. "github.com/mk6i/open-oscar-server/wire"
  12. )
  13. // OServiceService provides functionality for the OService food group, which
  14. // provides an assortment of services useful across multiple food groups.
  15. type OServiceService struct {
  16. buddyBroadcaster buddyBroadcaster
  17. cfg config.Config // todo remove
  18. logger *slog.Logger
  19. snacRateLimits wire.SNACRateLimits
  20. timeNow func() time.Time
  21. // how often MonitorRateLimits observes changes; a field so tests can shorten it
  22. rateLimitMonitorInterval time.Duration
  23. chatRoomManager ChatRoomRegistry
  24. cookieIssuer CookieBaker
  25. messageRelayer MessageRelayer
  26. chatMessageRelayer ChatMessageRelayer
  27. profileManager ProfileManager
  28. offlineMessageManager OfflineMessageManager
  29. feedbagManager FeedbagManager
  30. }
  31. // NewOServiceService creates a new instance of NewOServiceService.
  32. func NewOServiceService(
  33. cfg config.Config,
  34. messageRelayer MessageRelayer,
  35. logger *slog.Logger,
  36. cookieIssuer CookieBaker,
  37. chatRoomManager ChatRoomRegistry,
  38. relationshipFetcher RelationshipFetcher,
  39. sessionRetriever SessionRetriever,
  40. bartItemManager BARTItemManager,
  41. snacRateLimits wire.SNACRateLimits,
  42. chatMessageRelayer ChatMessageRelayer,
  43. profileManager ProfileManager,
  44. offlineMessageManager OfflineMessageManager,
  45. feedbagManager FeedbagManager,
  46. ) *OServiceService {
  47. return &OServiceService{
  48. cookieIssuer: cookieIssuer,
  49. messageRelayer: messageRelayer,
  50. buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
  51. cfg: cfg,
  52. logger: logger,
  53. snacRateLimits: snacRateLimits,
  54. timeNow: time.Now,
  55. rateLimitMonitorInterval: time.Second,
  56. chatRoomManager: chatRoomManager,
  57. chatMessageRelayer: chatMessageRelayer,
  58. profileManager: profileManager,
  59. offlineMessageManager: offlineMessageManager,
  60. feedbagManager: feedbagManager,
  61. }
  62. }
  63. // ClientVersions informs the server what food group versions the client
  64. // supports and returns to the client what food group versions it supports.
  65. // This method simply regurgitates versions supplied by the client in inBody
  66. // back to the client in a OServiceHostVersions SNAC. The server doesn't
  67. // attempt to accommodate any particular food group version. The server
  68. // implicitly accommodates any food group version for Windows AIM clients 5.x.
  69. // It returns SNAC wire.OServiceHostVersions containing the server's supported
  70. // food group versions followed by SNAC wire.OServiceMotd containing Message of
  71. // the Day. MOTD is sent here because some clients such as Jimm wait for it
  72. // before sending RateParamsQuery, causing the login flow to stall if omitted.
  73. // todo this documentation
  74. func (s OServiceService) ClientVersions(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x01_0x17_OServiceClientVersions) []wire.SNACMessage {
  75. var versions [wire.MDir + 1]uint16
  76. if len(inBody.Versions)%2 != 0 {
  77. s.logger.ErrorContext(ctx, "got uneven food group length")
  78. return nil
  79. }
  80. for i := 0; i < len(inBody.Versions); i += 2 {
  81. fg := inBody.Versions[i]
  82. if fg < wire.OService || fg > wire.MDir {
  83. s.logger.ErrorContext(ctx, "invalid food group ID", "id", fg)
  84. continue
  85. }
  86. ver := inBody.Versions[i+1]
  87. if ver < 1 {
  88. s.logger.ErrorContext(ctx, "invalid food group version", "version", ver)
  89. continue
  90. }
  91. versions[fg] = ver
  92. }
  93. instance.SetFoodGroupVersions(versions)
  94. return []wire.SNACMessage{
  95. {
  96. Frame: wire.SNACFrame{
  97. FoodGroup: wire.OService,
  98. SubGroup: wire.OServiceHostVersions,
  99. RequestID: inFrame.RequestID,
  100. },
  101. Body: wire.SNAC_0x01_0x18_OServiceHostVersions(inBody),
  102. },
  103. {
  104. Frame: wire.SNACFrame{
  105. FoodGroup: wire.OService,
  106. SubGroup: wire.OServiceMotd,
  107. RequestID: wire.ReqIDFromServer,
  108. },
  109. Body: wire.SNAC_0x01_0x13_OServiceMOTD{
  110. MessageType: 0x0004,
  111. TLVRestBlock: wire.TLVRestBlock{
  112. TLVList: wire.TLVList{
  113. wire.NewTLVBE(wire.OServiceTLVTagsMOTDMessage, "Welcome to Open OSCAR Server"),
  114. },
  115. },
  116. },
  117. },
  118. }
  119. }
  120. // RateParamsQuery returns SNAC rate limits. It returns SNAC
  121. // wire.OServiceRateParamsReply containing rate limits for all food groups
  122. // supported by this server.
  123. //
  124. // The purpose of this method is to convey per-SNAC server-side rate limits to
  125. // the client. The response consists of two main parts: rate classes and rate
  126. // groups. Rate classes define limits based on specific parameters, while rate
  127. // groups associate these limits with relevant SNAC types.
  128. //
  129. // The current implementation does not enforce server-side rate limiting.
  130. // Instead, the provided values inform the client about the recommended
  131. // client-side rate limits.
  132. //
  133. // AIM clients silently fail when they expect a rate limit rule that does not
  134. // exist in this response. When support for a new food group is added to the
  135. // server, update this function accordingly.
  136. func (s OServiceService) RateParamsQuery(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) wire.SNACMessage {
  137. // not contain LastTime and CurrentStatus fields.
  138. var limits = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
  139. RateClasses: []wire.RateParamsSNAC{},
  140. RateGroups: []struct {
  141. ID uint16
  142. Pairs []struct {
  143. FoodGroup uint16
  144. SubGroup uint16
  145. } `oscar:"count_prefix=uint16"`
  146. }{
  147. {
  148. ID: 1,
  149. Pairs: []struct {
  150. FoodGroup uint16
  151. SubGroup uint16
  152. }{},
  153. },
  154. {
  155. ID: 2,
  156. Pairs: []struct {
  157. FoodGroup uint16
  158. SubGroup uint16
  159. }{},
  160. },
  161. {
  162. ID: 3,
  163. Pairs: []struct {
  164. FoodGroup uint16
  165. SubGroup uint16
  166. }{},
  167. },
  168. {
  169. ID: 4,
  170. Pairs: []struct {
  171. FoodGroup uint16
  172. SubGroup uint16
  173. }{},
  174. },
  175. {
  176. ID: 5,
  177. Pairs: []struct {
  178. FoodGroup uint16
  179. SubGroup uint16
  180. }{},
  181. },
  182. },
  183. }
  184. for _, class := range instance.RateLimitStates() {
  185. str := wire.RateParamsSNAC{
  186. ID: uint16(class.ID),
  187. WindowSize: uint32(class.WindowSize),
  188. ClearLevel: uint32(class.ClearLevel),
  189. AlertLevel: uint32(class.AlertLevel),
  190. LimitLevel: uint32(class.LimitLevel),
  191. DisconnectLevel: uint32(class.DisconnectLevel),
  192. CurrentLevel: uint32(class.CurrentLevel),
  193. MaxLevel: uint32(class.MaxLevel),
  194. }
  195. if instance.FoodGroupVersions()[wire.OService] > 1 {
  196. str.V2Params = &struct {
  197. LastTime uint32
  198. DroppingSNACs uint8
  199. }{
  200. LastTime: uint32(s.timeNow().Add(-time.Second).Unix()),
  201. }
  202. }
  203. limits.RateClasses = append(limits.RateClasses, str)
  204. }
  205. for snacClass := range s.snacRateLimits.All() {
  206. classID := int(snacClass.RateLimitClass) - 1
  207. limits.RateGroups[classID].Pairs = append(limits.RateGroups[classID].Pairs,
  208. struct {
  209. FoodGroup uint16
  210. SubGroup uint16
  211. }{FoodGroup: snacClass.FoodGroup, SubGroup: snacClass.SubGroup})
  212. }
  213. return wire.SNACMessage{
  214. Frame: wire.SNACFrame{
  215. FoodGroup: wire.OService,
  216. SubGroup: wire.OServiceRateParamsReply,
  217. RequestID: inFrame.RequestID,
  218. },
  219. Body: limits,
  220. }
  221. }
  222. // UserInfoQuery returns SNAC wire.OServiceUserInfoUpdate containing
  223. // the user's info.
  224. func (s OServiceService) UserInfoQuery(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) wire.SNACMessage {
  225. return wire.SNACMessage{
  226. Frame: wire.SNACFrame{
  227. FoodGroup: wire.OService,
  228. SubGroup: wire.OServiceUserInfoUpdate,
  229. RequestID: inFrame.RequestID,
  230. },
  231. Body: newOServiceUserInfoUpdate(instance),
  232. }
  233. }
  234. // SetUserInfoFields updates user info fields (e.g., invisible, away) and broadcasts
  235. // presence changes to buddies. Returns an updated user info message.
  236. func (s OServiceService) SetUserInfoFields(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (wire.SNACMessage, error) {
  237. if status, hasStatus := inBody.Uint32BE(wire.OServiceUserInfoStatus); hasStatus {
  238. instance.SetUserStatusBitmask(status)
  239. if instance.Session().Invisible() {
  240. if err := s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, instance.IdentScreenName()); err != nil {
  241. return wire.SNACMessage{}, err
  242. }
  243. } else {
  244. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  245. return wire.SNACMessage{}, err
  246. }
  247. }
  248. }
  249. if dcBytes, hasDC := inBody.Bytes(wire.OServiceUserInfoICQDC); hasDC {
  250. var dc wire.ICQDCInfo
  251. if err := wire.UnmarshalBE(&dc, bytes.NewReader(dcBytes)); err != nil {
  252. return wire.SNACMessage{}, err
  253. }
  254. instance.SetICQDCInfo(dc)
  255. }
  256. // reflect the status of this instance back to the caller, even though
  257. // it does not reflect aggregated state of the session. this is necessary
  258. // for the "invisible" button to properly toggle on the client.
  259. info := instance.Session().TLVUserInfo()
  260. info.Set(wire.NewTLVBE(wire.OServiceUserInfoStatus, instance.UserStatusBitmask()))
  261. return wire.SNACMessage{
  262. Frame: wire.SNACFrame{
  263. FoodGroup: wire.OService,
  264. SubGroup: wire.OServiceUserInfoUpdate,
  265. RequestID: inFrame.RequestID,
  266. },
  267. Body: wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
  268. UserInfo: []wire.TLVUserInfo{info},
  269. },
  270. }, nil
  271. }
  272. // IdleNotification sets the user idle time.
  273. // Set session idle time to the value of bodyIn.IdleTime. Return a user arrival
  274. // message to all users who have this user on their buddy list.
  275. func (s OServiceService) IdleNotification(ctx context.Context, instance *state.SessionInstance, inBody wire.SNAC_0x01_0x11_OServiceIdleNotification) error {
  276. if inBody.IdleTime == 0 {
  277. instance.UnsetIdle()
  278. } else {
  279. instance.SetIdle(time.Duration(inBody.IdleTime) * time.Second)
  280. }
  281. return s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo())
  282. }
  283. // SetPrivacyFlags sets client privacy settings. Currently, there's no action
  284. // to take when these flags are set. This method simply logs the flags set by
  285. // the client.
  286. func (s OServiceService) SetPrivacyFlags(ctx context.Context, inBody wire.SNAC_0x01_0x14_OServiceSetPrivacyFlags) {
  287. attrs := slog.Group("request",
  288. slog.String("food_group", wire.FoodGroupName(wire.OService)),
  289. slog.String("sub_group", wire.SubGroupName(wire.OService, wire.OServiceSetPrivacyFlags)))
  290. if inBody.MemberFlag() {
  291. s.logger.LogAttrs(ctx, slog.LevelDebug, "client set member privacy flag, but we're not going to do anything", attrs)
  292. }
  293. if inBody.IdleFlag() {
  294. s.logger.LogAttrs(ctx, slog.LevelDebug, "client set idle privacy flag, but we're not going to do anything", attrs)
  295. }
  296. }
  297. // ProbeReq responds to client probe requests. Some ICQ clients send probe
  298. // requests to test server connectivity before authenticating. This returns a
  299. // simple ProbeAck to indicate the server is responsive.
  300. func (s OServiceService) ProbeReq(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
  301. return wire.SNACMessage{
  302. Frame: wire.SNACFrame{
  303. FoodGroup: wire.OService,
  304. SubGroup: wire.OServiceProbeAck,
  305. RequestID: inFrame.RequestID,
  306. },
  307. }
  308. }
  309. // RateParamsSubAdd subscribes to rate parameter changes. AOL's OSCAR spec says
  310. // that notifications will be queued after calling this method. I don't see the
  311. // point of doing that since all clients appear to call RateParamsQuery at
  312. // sign-on for all rate classes.
  313. func (s OServiceService) RateParamsSubAdd(ctx context.Context, instance *state.SessionInstance, inBody wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
  314. ids := make([]wire.RateLimitClassID, 0, len(inBody.ClassIDs))
  315. for _, id := range inBody.ClassIDs {
  316. if id < 1 || id > 5 {
  317. s.logger.DebugContext(ctx, "snac class ID out of range")
  318. continue
  319. }
  320. ids = append(ids, wire.RateLimitClassID(id))
  321. }
  322. if len(ids) == 0 {
  323. return
  324. }
  325. s.logger.DebugContext(ctx, "subscribing to rate limit updates", "classes", ids)
  326. instance.Session().SubscribeRateLimits(ids)
  327. }
  328. // HostOnline returns SNAC wire.OServiceHostOnline containing the list of food
  329. // groups supported by the particular service.
  330. func (s OServiceService) HostOnline(service uint16) wire.SNACMessage {
  331. switch service {
  332. case wire.Admin:
  333. return wire.SNACMessage{
  334. Frame: wire.SNACFrame{
  335. FoodGroup: wire.OService,
  336. SubGroup: wire.OServiceHostOnline,
  337. RequestID: wire.ReqIDFromServer,
  338. },
  339. Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
  340. FoodGroups: []uint16{
  341. wire.OService,
  342. wire.Admin,
  343. },
  344. },
  345. }
  346. case wire.Alert:
  347. return wire.SNACMessage{
  348. Frame: wire.SNACFrame{
  349. FoodGroup: wire.OService,
  350. SubGroup: wire.OServiceHostOnline,
  351. RequestID: wire.ReqIDFromServer,
  352. },
  353. Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
  354. FoodGroups: []uint16{
  355. wire.Alert,
  356. wire.OService,
  357. },
  358. },
  359. }
  360. case wire.BART:
  361. return wire.SNACMessage{
  362. Frame: wire.SNACFrame{
  363. FoodGroup: wire.OService,
  364. SubGroup: wire.OServiceHostOnline,
  365. RequestID: wire.ReqIDFromServer,
  366. },
  367. Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
  368. FoodGroups: []uint16{
  369. wire.BART,
  370. wire.OService,
  371. },
  372. },
  373. }
  374. case wire.BOS:
  375. return wire.SNACMessage{
  376. Frame: wire.SNACFrame{
  377. FoodGroup: wire.OService,
  378. SubGroup: wire.OServiceHostOnline,
  379. RequestID: wire.ReqIDFromServer,
  380. },
  381. Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
  382. FoodGroups: []uint16{
  383. wire.Alert,
  384. wire.BART,
  385. wire.Buddy,
  386. wire.Feedbag,
  387. wire.ICBM,
  388. wire.ICQ,
  389. wire.Locate,
  390. wire.OService,
  391. wire.PermitDeny,
  392. wire.UserLookup,
  393. wire.Invite,
  394. wire.Popup,
  395. wire.Stats,
  396. },
  397. },
  398. }
  399. case wire.Chat:
  400. return wire.SNACMessage{
  401. Frame: wire.SNACFrame{
  402. FoodGroup: wire.OService,
  403. SubGroup: wire.OServiceHostOnline,
  404. RequestID: wire.ReqIDFromServer,
  405. },
  406. Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
  407. FoodGroups: []uint16{
  408. wire.OService,
  409. wire.Chat,
  410. },
  411. },
  412. }
  413. case wire.ChatNav:
  414. return wire.SNACMessage{
  415. Frame: wire.SNACFrame{
  416. FoodGroup: wire.OService,
  417. SubGroup: wire.OServiceHostOnline,
  418. RequestID: wire.ReqIDFromServer,
  419. },
  420. Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
  421. FoodGroups: []uint16{
  422. wire.ChatNav,
  423. wire.OService,
  424. },
  425. },
  426. }
  427. case wire.ODir:
  428. return wire.SNACMessage{
  429. Frame: wire.SNACFrame{
  430. FoodGroup: wire.OService,
  431. SubGroup: wire.OServiceHostOnline,
  432. RequestID: wire.ReqIDFromServer,
  433. },
  434. Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
  435. FoodGroups: []uint16{
  436. wire.ODir,
  437. wire.OService,
  438. },
  439. },
  440. }
  441. }
  442. return wire.SNACMessage{
  443. Frame: wire.SNACFrame{
  444. FoodGroup: wire.OService,
  445. SubGroup: wire.OServiceErr,
  446. },
  447. }
  448. }
  449. // MonitorRateLimits observes account-wide rate limit changes on a fixed cadence
  450. // and broadcasts each transition to every instance in the session.
  451. //
  452. // Session.ObserveRateChanges is a single-consumer delta — it reports each
  453. // transition once, then overwrites its baseline — so one monitor per account is
  454. // the only correct consumer.
  455. //
  456. // Only classes a client on the account has subscribed to are broadcast. Fanout is
  457. // account-wide: the budget is shared, so a connection that spent nothing cannot
  458. // send either. That does mean an idle Web API tab raises its rate limit banner
  459. // when another tab spends the budget.
  460. //
  461. // Start it once per account from a Session.RunOnce block, with a server-lifetime
  462. // context; it runs until the session closes or that context is cancelled.
  463. func (s OServiceService) MonitorRateLimits(ctx context.Context, session *state.Session) {
  464. ticker := time.NewTicker(s.rateLimitMonitorInterval)
  465. defer ticker.Stop()
  466. for {
  467. select {
  468. case <-ctx.Done(): // server shutdown
  469. return
  470. case <-session.Closed(): // account signed off; a later sign-on starts a fresh monitor
  471. return
  472. case <-ticker.C:
  473. now := s.timeNow()
  474. classDelta, stateDelta := session.ObserveRateChanges(now)
  475. if len(classDelta) == 0 && len(stateDelta) == 0 {
  476. continue
  477. }
  478. instances := session.Instances()
  479. for _, curRate := range classDelta {
  480. s.logger.DebugContext(ctx, "rate limit class changed", "class", curRate.ID)
  481. for _, inst := range instances {
  482. inst.RelayMessageToInstance(buildRateLimitUpdate(1, curRate, inst, now))
  483. }
  484. }
  485. for _, curRate := range stateDelta {
  486. var code uint16
  487. switch curRate.CurrentStatus {
  488. case wire.RateLimitStatusLimited:
  489. code = 3
  490. case wire.RateLimitStatusAlert:
  491. code = 2
  492. case wire.RateLimitStatusClear:
  493. code = 4
  494. case wire.RateLimitStatusDisconnect:
  495. continue // the connection is torn down anyway
  496. }
  497. s.logger.DebugContext(ctx, "rate limit state changed",
  498. "class", curRate.ID,
  499. "state", curRate.CurrentStatus)
  500. for _, inst := range instances {
  501. inst.RelayMessageToInstance(buildRateLimitUpdate(code, curRate, inst, now))
  502. }
  503. }
  504. }
  505. }
  506. }
  507. // buildRateLimitUpdate constructs a SNAC message notifying the client of a rate limit
  508. // threshold update or a change in rate limiting status for a specific class.
  509. //
  510. // The message format varies depending on the client's supported protocol version.
  511. // If OService version 2 or higher is supported, additional metadata such as
  512. // time since last status change and whether SNACs are currently being dropped
  513. // will be included.
  514. func buildRateLimitUpdate(code uint16, curRate state.RateClassState, instance *state.SessionInstance, now time.Time) wire.SNACMessage {
  515. var droppingSNACs uint8
  516. if curRate.CurrentStatus == wire.RateLimitStatusLimited {
  517. droppingSNACs = 1
  518. }
  519. rate := wire.RateParamsSNAC{
  520. ID: uint16(curRate.ID),
  521. WindowSize: uint32(curRate.WindowSize),
  522. ClearLevel: uint32(curRate.ClearLevel),
  523. AlertLevel: uint32(curRate.AlertLevel),
  524. LimitLevel: uint32(curRate.LimitLevel),
  525. DisconnectLevel: uint32(curRate.DisconnectLevel),
  526. CurrentLevel: uint32(curRate.CurrentLevel),
  527. MaxLevel: uint32(curRate.MaxLevel),
  528. }
  529. if instance.FoodGroupVersions()[wire.OService] > 1 {
  530. rate.V2Params = &struct {
  531. LastTime uint32
  532. DroppingSNACs uint8
  533. }{
  534. LastTime: uint32(max(0, now.Unix()-curRate.LastTime.Unix())),
  535. DroppingSNACs: droppingSNACs,
  536. }
  537. }
  538. return wire.SNACMessage{
  539. Frame: wire.SNACFrame{
  540. FoodGroup: wire.OService,
  541. SubGroup: wire.OServiceRateParamChange,
  542. RequestID: wire.ReqIDFromServer,
  543. },
  544. Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
  545. Code: code,
  546. Rate: rate,
  547. },
  548. }
  549. }
  550. // ServiceRequest handles service discovery, providing a host name and metadata
  551. // for connecting to the food group service specified in inFrame.
  552. func (s OServiceService) ServiceRequest(ctx context.Context, service uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x01_0x04_OServiceServiceRequest, listenerGroup config.ListenerGroup) (wire.SNACMessage, error) {
  553. if service != wire.BOS {
  554. return wire.SNACMessage{
  555. Frame: wire.SNACFrame{
  556. FoodGroup: wire.OService,
  557. SubGroup: wire.OServiceErr,
  558. RequestID: inFrame.RequestID,
  559. },
  560. Body: wire.SNACError{
  561. Code: wire.ErrorCodeNotSupportedByHost,
  562. },
  563. }, nil
  564. }
  565. fnIssueCookie := func(val any) ([]byte, error) {
  566. buf := &bytes.Buffer{}
  567. if err := wire.MarshalBE(val, buf); err != nil {
  568. return nil, err
  569. }
  570. return s.cookieIssuer.Issue(buf.Bytes())
  571. }
  572. cookie, err := func() ([]byte, error) {
  573. switch inBody.FoodGroup {
  574. case wire.Admin, wire.Alert, wire.BART, wire.ChatNav, wire.ODir:
  575. return fnIssueCookie(state.ServerCookie{
  576. Service: inBody.FoodGroup,
  577. ScreenName: instance.DisplayScreenName(),
  578. SessionNum: instance.Num(),
  579. })
  580. case wire.Chat:
  581. roomMeta, ok := inBody.Bytes(0x01)
  582. if !ok {
  583. return nil, errors.New("missing room info")
  584. }
  585. roomSNAC := wire.SNAC_0x01_0x04_TLVRoomInfo{}
  586. if err := wire.UnmarshalBE(&roomSNAC, bytes.NewBuffer(roomMeta)); err != nil {
  587. return nil, err
  588. }
  589. room, err := s.chatRoomManager.ChatRoomByCookie(ctx, roomSNAC.Cookie)
  590. if err != nil {
  591. return nil, fmt.Errorf("unable to retrieve room info: %w", err)
  592. }
  593. return fnIssueCookie(state.ServerCookie{
  594. Service: wire.Chat,
  595. ChatCookie: room.Cookie(),
  596. ScreenName: instance.DisplayScreenName(),
  597. SessionNum: instance.Num(),
  598. })
  599. case wire.OService:
  600. // Linked Account signon request
  601. _, ok := inBody.Bytes(0x0028)
  602. if !ok {
  603. return nil, errors.New("unknown OService request")
  604. }
  605. snBytes, ok := inBody.Bytes(0x01)
  606. if !ok {
  607. return nil, errors.New("new session request missing linked screenname TLV 0x01")
  608. }
  609. linkedScreenName := state.NewIdentScreenName(string(snBytes))
  610. s.logger.Debug("Linked Account signon request", "primary", instance.IdentScreenName(), "linked", linkedScreenName.String())
  611. items, err := s.feedbagManager.Feedbag(ctx, instance.IdentScreenName())
  612. if err != nil {
  613. return nil, fmt.Errorf("unable to check linked account: %w", err)
  614. }
  615. if !state.NewFeedbagList(items, nil).HasLinkedScreenName(linkedScreenName.String()) {
  616. return nil, errors.New("linked account session requested but accounts are not linked")
  617. }
  618. return fnIssueCookie(state.ServerCookie{
  619. Service: wire.BOS,
  620. ScreenName: state.DisplayScreenName(snBytes),
  621. MultiConnFlag: uint8(instance.MultiConnFlag()),
  622. })
  623. default:
  624. return nil, nil
  625. }
  626. }()
  627. if err != nil {
  628. return wire.SNACMessage{}, err
  629. }
  630. if cookie == nil {
  631. s.logger.InfoContext(ctx, "client service request for unsupported service", "food_group", wire.FoodGroupName(inBody.FoodGroup))
  632. return wire.SNACMessage{
  633. Frame: wire.SNACFrame{
  634. FoodGroup: wire.OService,
  635. SubGroup: wire.OServiceErr,
  636. RequestID: inFrame.RequestID,
  637. },
  638. Body: wire.SNACError{
  639. Code: wire.ErrorCodeServiceUnavailable,
  640. },
  641. }, nil
  642. }
  643. host := listenerGroup.BOSAdvertisedHostPlain
  644. stateCode := wire.OServiceServiceResponseSSLStateNotUsed
  645. if inBody.HasTag(wire.OserviceTLVTagsSSLUseSSL) {
  646. if listenerGroup.HasSSL() {
  647. host = listenerGroup.BOSAdvertisedHostSSL
  648. stateCode = wire.OServiceServiceResponseSSLStateResume
  649. } else {
  650. // redirect to the plaintext host and let the client decide whether
  651. // to downgrade or give up
  652. s.logger.DebugContext(ctx, "service request for SSL but the listener doesn't support SSL")
  653. }
  654. }
  655. return wire.SNACMessage{
  656. Frame: wire.SNACFrame{
  657. FoodGroup: wire.OService,
  658. SubGroup: wire.OServiceServiceResponse,
  659. RequestID: inFrame.RequestID,
  660. },
  661. Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
  662. TLVRestBlock: wire.TLVRestBlock{
  663. TLVList: wire.TLVList{
  664. wire.NewTLVBE(wire.OServiceTLVTagsGroupID, inBody.FoodGroup),
  665. wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, host),
  666. wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, cookie),
  667. wire.NewTLVBE(wire.OServiceTLVTagsSSLState, stateCode),
  668. },
  669. },
  670. },
  671. }, nil
  672. }
  673. // ClientOnline runs when the current user is ready to join.
  674. // If BOS:
  675. // - Announce current user's arrival to users who have the current user on their buddy list,
  676. // but only when the contact list is ready (feedbag initialized or client-side buddy
  677. // list loaded). Clients that send ClientOnline before feedbag activation get the
  678. // initial broadcast from FeedbagService.Use instead.
  679. //
  680. // If Chat:
  681. // - Send current user the chat room metadata
  682. // - Announce current user's arrival to other chat room participants
  683. // - Send current user the chat room participant list
  684. func (s OServiceService) ClientOnline(ctx context.Context, service uint16, inBody wire.SNAC_0x01_0x02_OServiceClientOnline, instance *state.SessionInstance) error {
  685. instance.SetSignonComplete()
  686. switch service {
  687. case wire.BOS:
  688. // AIM order: feedbag or client-side buddy list before ClientOnline.
  689. if instance.ContactsInit() {
  690. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, nil, false); err != nil {
  691. return fmt.Errorf("unable to send buddy arrival notification: %w", err)
  692. }
  693. }
  694. msg := wire.SNACMessage{
  695. Frame: wire.SNACFrame{
  696. FoodGroup: wire.Stats,
  697. SubGroup: wire.StatsSetMinReportInterval,
  698. RequestID: wire.ReqIDFromServer,
  699. },
  700. Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
  701. MinReportInterval: 1,
  702. },
  703. }
  704. s.messageRelayer.RelayToScreenName(ctx, instance.IdentScreenName(), msg)
  705. // set stored profile
  706. if instance.KerberosAuth() {
  707. // normally, the SupportHostSig TLV indicates that the profile should
  708. // be stored server-side. however, some AIM 6 clients expect server-side
  709. // profiles but do not send this TLV. in order to cover all bases, just
  710. // save the profile for all kerberos-based clients.
  711. profile, err := s.profileManager.Profile(ctx, instance.IdentScreenName())
  712. if err != nil {
  713. return fmt.Errorf("unable to reload profile: %w", err)
  714. }
  715. if !profile.IsZero() {
  716. instance.SetProfile(profile)
  717. // notify client that the server-side profile is ready for retrieval
  718. s.messageRelayer.RelayToSelf(ctx, instance, wire.SNACMessage{
  719. Frame: wire.SNACFrame{
  720. FoodGroup: wire.OService,
  721. SubGroup: wire.OServiceUserInfoUpdate,
  722. },
  723. Body: newOServiceUserInfoUpdate(instance),
  724. })
  725. }
  726. }
  727. if instance.UIN() == 0 && instance.OfflineMsgCount() > 0 {
  728. if err := s.sendOfflineMessageNotification(ctx, instance); err != nil {
  729. return fmt.Errorf("send offline message notification: %w", err)
  730. }
  731. }
  732. if !s.cfg.DisableMultiLoginNotif && instance.Session().InstanceCount() > 1 {
  733. if err := s.sendMultipleInstanceNotification(ctx, instance); err != nil {
  734. return fmt.Errorf("send multiple instance notification: %w", err)
  735. }
  736. }
  737. return nil
  738. case wire.Chat:
  739. room, err := s.chatRoomManager.ChatRoomByCookie(ctx, instance.ChatRoomCookie())
  740. if err != nil {
  741. return fmt.Errorf("error getting chat room: %w", err)
  742. }
  743. // Do not change the order of the following 3 methods. macOS client v4.0.9
  744. // requires this exact sequence, otherwise the chat session prematurely
  745. // closes seconds after users join a chat room.
  746. setOnlineChatUsers(ctx, instance, s.chatMessageRelayer)
  747. sendChatRoomInfoUpdate(ctx, instance, s.chatMessageRelayer, room)
  748. alertUserJoined(ctx, instance, s.chatMessageRelayer)
  749. return nil
  750. default:
  751. s.logger.DebugContext(ctx, "client is online", "group_versions", inBody.GroupVersions)
  752. return nil
  753. }
  754. }
  755. // sendOfflineMessageNotification sends an IM notifying the user of their
  756. // offline message count and resets the count to zero.
  757. func (s OServiceService) sendOfflineMessageNotification(ctx context.Context, instance *state.SessionInstance) error {
  758. if err := s.offlineMessageManager.SetOfflineMsgCount(ctx, instance.IdentScreenName(), 0); err != nil {
  759. return fmt.Errorf("deleting offline messages: %w", err)
  760. }
  761. msg := fmt.Sprintf("You just received %d IM(s) while you were offline. If you do "+
  762. "not wish to receive offline messages, please go to "+
  763. "<a href=\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ&start_radio=1&pp=ygUJcmljayByb2xsoAcB\">IM Settings</a>.", instance.OfflineMsgCount())
  764. message, err := systemMessage(msg)
  765. if err != nil {
  766. return err
  767. }
  768. s.messageRelayer.RelayToScreenName(ctx, instance.IdentScreenName(), message)
  769. instance.Session().SetOfflineMsgCount(0)
  770. return nil
  771. }
  772. // sendMultipleInstanceNotification sends an IM notifying the user that their
  773. // account is signed in to multiple locations.
  774. func (s OServiceService) sendMultipleInstanceNotification(ctx context.Context, instance *state.SessionInstance) error {
  775. msg := fmt.Sprintf("Your screen name (%s) is now signed into Open OSCAR Server in %d locations. Click "+
  776. "<a href=\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ&start_radio=1&pp=ygUJcmljayByb2xsoAcB\">here</a> "+
  777. "for more information.", instance.DisplayScreenName(), instance.Session().InstanceCount())
  778. message, err := systemMessage(msg)
  779. if err != nil {
  780. return err
  781. }
  782. s.messageRelayer.RelayToOtherInstances(ctx, instance, message)
  783. return nil
  784. }
  785. func systemMessage(msg string) (wire.SNACMessage, error) {
  786. frags, err := wire.ICBMFragmentList(msg)
  787. if err != nil {
  788. return wire.SNACMessage{}, fmt.Errorf("creating ICBM fragments: %w", err)
  789. }
  790. return wire.SNACMessage{
  791. Frame: wire.SNACFrame{
  792. FoodGroup: wire.ICBM,
  793. SubGroup: wire.ICBMChannelMsgToClient,
  794. RequestID: wire.ReqIDFromServer,
  795. },
  796. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  797. ChannelID: wire.ICBMChannelIM,
  798. TLVUserInfo: wire.TLVUserInfo{
  799. ScreenName: "OOS System Msg",
  800. },
  801. TLVRestBlock: wire.TLVRestBlock{
  802. TLVList: []wire.TLV{
  803. wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags),
  804. },
  805. },
  806. },
  807. }, nil
  808. }
  809. // newOServiceUserInfoUpdate constructs SNAC(0x01,0x0F) for user info updates.
  810. // For OService version 4 and above, it appends a duplicate TLVUserInfo block.
  811. // AIM 6+ expects at least two user info blocks to support multi-session:
  812. // the first represents overall state; subsequent ones represent client instances.
  813. func newOServiceUserInfoUpdate(instance *state.SessionInstance) wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate {
  814. info := instance.Session().TLVUserInfo()
  815. userInfo := []wire.TLVUserInfo{info}
  816. // set registration date
  817. userInfo[0].Append(wire.NewTLVBE(wire.OServiceUserInfoMemberSince, uint32(instance.Session().MemberSince().Unix())))
  818. // set sign-on time
  819. userInfo[0].Append(wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(instance.SignonTime().Unix())))
  820. // set current session length (seconds)
  821. userInfo[0].Append(wire.NewTLVBE(wire.OServiceUserInfoOnlineTime, uint32(time.Since(instance.SignonTime()).Seconds())))
  822. if instance.FoodGroupVersions()[wire.OService] >= 4 {
  823. userInfo[0].Append(wire.NewTLVBE(wire.OServiceUserInfoMyInstanceNum, []byte{instance.Num()}))
  824. for _, cur := range instance.Session().Instances() {
  825. instanceInfo := wire.TLVUserInfo{
  826. ScreenName: cur.DisplayScreenName().String(),
  827. WarningLevel: cur.Warning(),
  828. }
  829. // sign-in timestamp
  830. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(cur.SignonTime().Unix())))
  831. // use the first instance as a template
  832. uFlags := cur.UserInfoBitmask()
  833. if cur.Session().Away() {
  834. uFlags |= wire.OServiceUserFlagUnavailable
  835. }
  836. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uFlags))
  837. // user status flags - user-level (shared)
  838. var statusBitmask uint32
  839. if cur.Invisible() {
  840. statusBitmask |= wire.OServiceUserStatusInvisible
  841. }
  842. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoStatus, statusBitmask))
  843. if cur == instance {
  844. if icon, hasIcon := cur.Session().BuddyIcon(); hasIcon {
  845. // set buddy icon metadata, if user has buddy icon
  846. if icon.Type != 0 {
  847. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoBARTInfo, icon))
  848. }
  849. }
  850. }
  851. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoOscarCaps, cur.Session().Caps()))
  852. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)))
  853. if cur == instance {
  854. profile := cur.Profile()
  855. if !profile.UpdateTime.IsZero() {
  856. // set profile update time if the profile was set
  857. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoSigTime, uint32(profile.UpdateTime.Unix())))
  858. }
  859. }
  860. instanceInfo.Append(wire.NewTLVBE(wire.OServiceUserInfoPrimaryInstance, []byte{cur.Num()}))
  861. userInfo = append(userInfo, instanceInfo)
  862. }
  863. }
  864. return wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
  865. UserInfo: userInfo,
  866. }
  867. }