oservice.go 30 KB

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