icbm.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. package foodgroup
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log/slog"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/patrickmn/go-cache"
  13. "golang.org/x/net/html"
  14. "github.com/mk6i/open-oscar-server/state"
  15. "github.com/mk6i/open-oscar-server/wire"
  16. )
  17. const (
  18. evilDelta = uint16(100)
  19. evilDeltaAnon = uint16(30)
  20. warningDecayPct = -50
  21. rateDecayInterval = 5 * time.Minute
  22. )
  23. // NewICBMService returns a new instance of ICBMService.
  24. func NewICBMService(bartItemManager BARTItemManager, messageRelayer MessageRelayer, offlineMessageSaver OfflineMessageManager, relationshipFetcher RelationshipFetcher, sessionRetriever SessionRetriever, userManager UserManager, feedbagManager FeedbagManager, contactPreAuthorizer ContactPreAuthorizer, snacRateLimits wire.SNACRateLimits, logger *slog.Logger) *ICBMService {
  25. return &ICBMService{
  26. relationshipFetcher: relationshipFetcher,
  27. buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
  28. messageRelayer: messageRelayer,
  29. offlineMessageSaver: offlineMessageSaver,
  30. offlineMessageManager: offlineMessageSaver,
  31. userManager: userManager,
  32. feedbagManager: feedbagManager,
  33. contactPreAuthorizer: contactPreAuthorizer,
  34. timeNow: time.Now,
  35. sessionRetriever: sessionRetriever,
  36. snacRateLimits: snacRateLimits,
  37. convoTracker: newConvoTracker(),
  38. logger: logger,
  39. interval: rateDecayInterval,
  40. forwardICQAuthEvents: func(ctx context.Context, sender state.IdentScreenName, recipient state.IdentScreenName, authMsg wire.ICBMCh4Message) error {
  41. return errors.New("forwardICQAuthEvents not implemented")
  42. },
  43. }
  44. }
  45. // ICBMService provides functionality for the ICBM food group, which is
  46. // responsible for sending and receiving instant messages and associated
  47. // functionality such as warning, typing events, etc.
  48. type ICBMService struct {
  49. relationshipFetcher RelationshipFetcher
  50. buddyBroadcaster buddyBroadcaster
  51. messageRelayer MessageRelayer
  52. offlineMessageSaver OfflineMessageManager
  53. userManager UserManager
  54. feedbagManager FeedbagManager
  55. contactPreAuthorizer ContactPreAuthorizer
  56. timeNow func() time.Time
  57. sessionRetriever SessionRetriever
  58. snacRateLimits wire.SNACRateLimits
  59. convoTracker *convoTracker
  60. logger *slog.Logger
  61. interval time.Duration
  62. offlineMessageManager OfflineMessageManager
  63. forwardICQAuthEvents func(ctx context.Context, sender state.IdentScreenName, recipient state.IdentScreenName, authMsg wire.ICBMCh4Message) error
  64. }
  65. // BridgeFeedbagService enables the ICBMService to forward legacy ICQ events to
  66. // the ICBM service.
  67. func (s *ICBMService) BridgeFeedbagService(service *FeedbagService) {
  68. s.forwardICQAuthEvents = service.ForwardICQAuthEvents
  69. }
  70. // ParameterQuery returns ICBM service parameters.
  71. func (s *ICBMService) ParameterQuery(_ context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
  72. return wire.SNACMessage{
  73. Frame: wire.SNACFrame{
  74. FoodGroup: wire.ICBM,
  75. SubGroup: wire.ICBMParameterReply,
  76. RequestID: inFrame.RequestID,
  77. },
  78. Body: wire.SNAC_0x04_0x05_ICBMParameterReply{
  79. MaxSlots: 100,
  80. ICBMFlags: 3,
  81. MaxIncomingICBMLen: 512,
  82. MaxSourceEvil: 999,
  83. MaxDestinationEvil: 999,
  84. MinInterICBMInterval: 0,
  85. },
  86. }
  87. }
  88. // ChannelMsgToHost relays the instant message SNAC wire.ICBMChannelMsgToHost
  89. // from the sender to the intended recipient. It returns wire.ICBMHostAck if
  90. // the wire.ICBMChannelMsgToHost message contains a request acknowledgement
  91. // flag.
  92. func (s *ICBMService) ChannelMsgToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error) {
  93. recip := state.NewIdentScreenName(inBody.ScreenName)
  94. rel, err := s.relationshipFetcher.Relationship(ctx, instance.IdentScreenName(), recip)
  95. if err != nil {
  96. return nil, err
  97. }
  98. switch {
  99. case rel.BlocksYou:
  100. return newICBMErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  101. case rel.YouBlock:
  102. return newICBMErr(inFrame.RequestID, wire.ErrorCodeInLocalPermitDeny), nil
  103. }
  104. recipSess := s.sessionRetriever.RetrieveSession(recip)
  105. if recipSess == nil {
  106. // check for TLV that indicates that the message should be saved offline.
  107. // For AIM 6/7, this is only set if the sender has the recipient on
  108. // their buddy list and they've seen them online at least once.
  109. if _, saveOffline := inBody.Bytes(wire.ICBMTLVStore); !saveOffline {
  110. return newICBMErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  111. }
  112. canSend, err := s.canSendOfflineMessage(ctx, inBody)
  113. if err != nil {
  114. return nil, err
  115. }
  116. if !canSend {
  117. return newICBMErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  118. }
  119. msg, err := s.sendOfflineMessage(ctx, instance, inFrame, inBody)
  120. if errors.Is(err, state.ErrNoUser) {
  121. return newICBMErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  122. }
  123. return msg, err
  124. }
  125. if inBody.ChannelID == wire.ICBMChannelICQ {
  126. if b, ok := inBody.Bytes(wire.ICBMTLVData); ok {
  127. authMsg := wire.ICBMCh4Message{}
  128. if err = wire.UnmarshalLE(&authMsg, bytes.NewReader(b)); err != nil {
  129. return nil, fmt.Errorf("failed to unmarshal ICBM authMsg: %w", err)
  130. }
  131. if authMsg.MessageType == wire.ICBMMsgTypeAuthReq ||
  132. authMsg.MessageType == wire.ICBMMsgTypeAuthDeny ||
  133. authMsg.MessageType == wire.ICBMMsgTypeAuthOK ||
  134. authMsg.MessageType == wire.ICBMMsgTypeAdded {
  135. if recipSess.UsesFeedbag() {
  136. return nil, s.forwardICQAuthEvents(ctx, instance.IdentScreenName(), recipSess.IdentScreenName(), authMsg)
  137. } else if authMsg.MessageType == wire.ICBMMsgTypeAuthOK {
  138. if err := s.contactPreAuthorizer.RecordPreAuth(ctx, instance.IdentScreenName(), recipSess.IdentScreenName()); err != nil {
  139. return nil, fmt.Errorf("RecordPreAuth: %w", err)
  140. }
  141. }
  142. }
  143. }
  144. }
  145. clientIM := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  146. Cookie: inBody.Cookie,
  147. ChannelID: inBody.ChannelID,
  148. TLVUserInfo: instance.Session().TLVUserInfo(),
  149. TLVRestBlock: wire.TLVRestBlock{},
  150. }
  151. for _, tlv := range inBody.TLVList {
  152. if tlv.Tag == wire.ICBMTLVRequestHostAck {
  153. // Exclude this TLV, because its presence breaks chat invitations
  154. // on macOS client v4.0.9.
  155. continue
  156. }
  157. if tlv.Tag == wire.ICBMTLVStore {
  158. // Strip the store message directive.
  159. continue
  160. }
  161. if tlv.Tag == wire.ICBMTLVSendTime {
  162. // Only the server stamps a send time, and only on a message replayed
  163. // out of the offline store. Forwarding the sender's would let them
  164. // pass a live message off as a stored one and date it at will.
  165. continue
  166. }
  167. if clientIM.ChannelID == wire.ICBMChannelRendezvous && tlv.Tag == wire.ICBMTLVData {
  168. if tlv, err = addExternalIP(instance, tlv); err != nil {
  169. return nil, fmt.Errorf("addExternalIP: %w", err)
  170. }
  171. }
  172. // Strip HTML from ICQ messages if recipient doesn't support XHTML.
  173. // AIM clients send HTML formatted messages that should be preserved.
  174. if instance.UIN() > 0 &&
  175. (clientIM.ChannelID == wire.ICBMChannelIM || clientIM.ChannelID == wire.ICBMChannelMIME) &&
  176. tlv.Tag == wire.ICBMTLVAOLIMData {
  177. if !recipSess.HasCap(wire.CapXHTMLIM) {
  178. if transformedTLV, err := stripHTMLFromICBMTLV(tlv); err == nil {
  179. tlv = transformedTLV
  180. }
  181. }
  182. }
  183. clientIM.Append(tlv)
  184. }
  185. if instance.TypingEventsEnabled() && (inBody.ChannelID == wire.ICBMChannelIM || inBody.ChannelID == wire.ICBMChannelMIME) {
  186. // tell the receiver that we want to receive their typing events
  187. clientIM.Append(wire.NewTLVBE(wire.ICBMTLVWantEvents, []byte{}))
  188. }
  189. if recipSess.Inactive() {
  190. s.messageRelayer.RelayToScreenName(ctx, recipSess.IdentScreenName(), wire.SNACMessage{
  191. Frame: wire.SNACFrame{
  192. FoodGroup: wire.ICBM,
  193. SubGroup: wire.ICBMChannelMsgToClient,
  194. RequestID: wire.ReqIDFromServer,
  195. },
  196. Body: clientIM,
  197. })
  198. } else {
  199. s.messageRelayer.RelayToScreenNameActiveOnly(ctx, recipSess.IdentScreenName(), wire.SNACMessage{
  200. Frame: wire.SNACFrame{
  201. FoodGroup: wire.ICBM,
  202. SubGroup: wire.ICBMChannelMsgToClient,
  203. RequestID: wire.ReqIDFromServer,
  204. },
  205. Body: clientIM,
  206. })
  207. }
  208. s.convoTracker.trackConvo(time.Now(), instance.IdentScreenName(), recipSess.IdentScreenName())
  209. if _, requestedConfirmation := inBody.Bytes(wire.ICBMTLVRequestHostAck); !requestedConfirmation {
  210. // don't ack message
  211. return nil, nil
  212. }
  213. // ack message back to sender
  214. return &wire.SNACMessage{
  215. Frame: wire.SNACFrame{
  216. FoodGroup: wire.ICBM,
  217. SubGroup: wire.ICBMHostAck,
  218. RequestID: inFrame.RequestID,
  219. },
  220. Body: wire.SNAC_0x04_0x0C_ICBMHostAck{
  221. Cookie: inBody.Cookie,
  222. ChannelID: inBody.ChannelID,
  223. ScreenName: inBody.ScreenName,
  224. },
  225. }, nil
  226. }
  227. // canSendOfflineMessage returns true if the user can send an offline message.
  228. //
  229. // For ICQ users, always return true. Todo: Check ICQ recipient's preferences.
  230. //
  231. // For AIM users, only return false if the recipient has specifically opted out
  232. // of receiving offline messages or they do not have a stored buddy list.
  233. func (s *ICBMService) canSendOfflineMessage(ctx context.Context, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (bool, error) {
  234. bag, err := s.feedbagManager.Feedbag(ctx, state.NewIdentScreenName(inBody.ScreenName))
  235. if err != nil {
  236. return false, fmt.Errorf("get feedbag failed: %w", err)
  237. }
  238. for _, item := range bag {
  239. if item.ClassID == wire.FeedbagClassIdBuddyPrefs {
  240. // wire.BuddyPref defaults AcceptOfflineIM to true when the bit is
  241. // absent, matching AIM 6.0+ behavior: the preference did not exist
  242. // prior to AIM 6, so users who never ran a capable client are
  243. // assumed to accept stored offline messages.
  244. return wire.BuddyPref(item.TLVList, wire.FeedbagBuddyPrefsAcceptOfflineIM), nil
  245. }
  246. }
  247. return true, nil
  248. }
  249. func (s *ICBMService) sendOfflineMessage(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error) {
  250. recip := state.NewIdentScreenName(inBody.ScreenName)
  251. offlineMsg := state.OfflineMessage{
  252. Message: inBody,
  253. Recipient: recip,
  254. Sender: instance.IdentScreenName(),
  255. Sent: s.timeNow().UTC(),
  256. }
  257. if _, err := s.offlineMessageSaver.SaveMessage(ctx, offlineMsg); err != nil {
  258. if errors.Is(err, state.ErrOfflineInboxFull) {
  259. return newICBMErr(
  260. inFrame.RequestID,
  261. wire.ErrorCodeNotLoggedOn,
  262. wire.NewTLVBE(wire.ErrorTLVErrorSubcode, wire.ICBMSubErrOfflineIMExceedMax),
  263. ), nil
  264. }
  265. return nil, fmt.Errorf("save ICBM offline message failed: %w", err)
  266. }
  267. if _, requestedConfirmation := inBody.Bytes(wire.ICBMTLVRequestHostAck); requestedConfirmation {
  268. // ack message back to sender
  269. return &wire.SNACMessage{
  270. Frame: wire.SNACFrame{
  271. FoodGroup: wire.ICBM,
  272. SubGroup: wire.ICBMHostAck,
  273. RequestID: inFrame.RequestID,
  274. },
  275. Body: wire.SNAC_0x04_0x0C_ICBMHostAck{
  276. Cookie: inBody.Cookie,
  277. ChannelID: inBody.ChannelID,
  278. ScreenName: inBody.ScreenName,
  279. },
  280. }, nil
  281. }
  282. return nil, nil
  283. }
  284. // addExternalIP appends the client's IP address to the TLV if it's an ICBM
  285. // rendezvous proposal/accept message.
  286. func addExternalIP(instance *state.SessionInstance, tlv wire.TLV) (wire.TLV, error) {
  287. frag := wire.ICBMCh2Fragment{}
  288. if err := wire.UnmarshalBE(&frag, bytes.NewReader(tlv.Value)); err != nil {
  289. return tlv, fmt.Errorf("wire.UnmarshalBE: %w", err)
  290. }
  291. if frag.Type != wire.ICBMRdvMessagePropose {
  292. return tlv, nil
  293. }
  294. if frag.HasTag(wire.ICBMRdvTLVTagsRequesterIP) && instance.RemoteAddr() != nil && instance.RemoteAddr().Addr().Is4() {
  295. ip := instance.RemoteAddr().Addr()
  296. // replace the IP set by the client with the actual IP seen by the
  297. // server. unlike AOL's original behavior, this allows NATed clients
  298. // to use rendezvous by replacing their LAN IP with the correct
  299. // external IP.
  300. frag.Set(wire.NewTLVBE(wire.ICBMRdvTLVTagsRequesterIP, ip.AsSlice()))
  301. // append the client's IP as seen by the server. the recipient uses
  302. // this to verify that the sender's claimed IP matches what the server
  303. // detects. although redundant since we override the requester IP
  304. // above, it remains required for client compatibility.
  305. frag.Append(wire.NewTLVBE(wire.ICBMRdvTLVTagsVerifiedIP, ip.AsSlice()))
  306. return wire.NewTLVBE(tlv.Tag, frag), nil
  307. }
  308. return tlv, nil
  309. }
  310. // stripHTML extracts plaintext from HTML content.
  311. func stripHTML(text []byte) []byte {
  312. if len(text) == 0 {
  313. return text
  314. }
  315. var result strings.Builder
  316. tok := html.NewTokenizer(strings.NewReader(string(text)))
  317. for {
  318. tt := tok.Next()
  319. switch tt {
  320. case html.TextToken:
  321. result.Write(tok.Text())
  322. case html.SelfClosingTagToken, html.StartTagToken:
  323. tn, _ := tok.TagName()
  324. if string(tn) == "br" {
  325. result.WriteByte('\n')
  326. }
  327. case html.ErrorToken:
  328. if tok.Err() == io.EOF {
  329. return []byte(result.String())
  330. }
  331. // on error return what we have
  332. return []byte(result.String())
  333. }
  334. }
  335. }
  336. // stripHTMLFromICBMTLV transforms an ICBMTLVAOLIMData TLV by stripping HTML
  337. // from the message text for clients that don't support XHTML.
  338. func stripHTMLFromICBMTLV(tlv wire.TLV) (wire.TLV, error) {
  339. var frags []wire.ICBMCh1Fragment
  340. if err := wire.UnmarshalBE(&frags, bytes.NewBuffer(tlv.Value)); err != nil {
  341. return tlv, fmt.Errorf("unmarshal ICBM fragments: %w", err)
  342. }
  343. modified := false
  344. for i, frag := range frags {
  345. if frag.ID == 1 { // 1 = message text
  346. msg := wire.ICBMCh1Message{}
  347. if err := wire.UnmarshalBE(&msg, bytes.NewBuffer(frag.Payload)); err != nil {
  348. continue
  349. }
  350. // Strip HTML from message text
  351. strippedText := stripHTML(msg.Text)
  352. if !bytes.Equal(strippedText, msg.Text) {
  353. msg.Text = strippedText
  354. // Remarshal the message
  355. msgBuf := bytes.Buffer{}
  356. if err := wire.MarshalBE(msg, &msgBuf); err != nil {
  357. continue
  358. }
  359. frags[i].Payload = msgBuf.Bytes()
  360. modified = true
  361. }
  362. }
  363. }
  364. if !modified {
  365. return tlv, nil
  366. }
  367. // Remarshal the fragments
  368. newValue, err := wire.MarshalICBMFragmentList(frags)
  369. if err != nil {
  370. return tlv, err
  371. }
  372. return wire.NewTLVBE(tlv.Tag, newValue), nil
  373. }
  374. // ClientEvent relays SNAC wire.ICBMClientEvent typing events from the
  375. // sender to the recipient.
  376. func (s *ICBMService) ClientEvent(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x14_ICBMClientEvent) error {
  377. blocked, err := s.relationshipFetcher.Relationship(ctx, instance.IdentScreenName(), state.NewIdentScreenName(inBody.ScreenName))
  378. switch {
  379. case err != nil:
  380. return err
  381. case blocked.BlocksYou || blocked.YouBlock:
  382. return nil
  383. default:
  384. recipient := state.NewIdentScreenName(inBody.ScreenName)
  385. s.messageRelayer.RelayToScreenNameActiveOnly(ctx, recipient, wire.SNACMessage{
  386. Frame: wire.SNACFrame{
  387. FoodGroup: wire.ICBM,
  388. SubGroup: wire.ICBMClientEvent,
  389. RequestID: inFrame.RequestID,
  390. },
  391. Body: wire.SNAC_0x04_0x14_ICBMClientEvent{
  392. Cookie: inBody.Cookie,
  393. ChannelID: inBody.ChannelID,
  394. ScreenName: string(instance.DisplayScreenName()),
  395. Event: inBody.Event,
  396. },
  397. })
  398. return nil
  399. }
  400. }
  401. func (s *ICBMService) ClientErr(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x0B_ICBMClientErr) error {
  402. s.messageRelayer.RelayToScreenName(ctx, state.NewIdentScreenName(inBody.ScreenName), wire.SNACMessage{
  403. Frame: wire.SNACFrame{
  404. FoodGroup: wire.ICBM,
  405. SubGroup: wire.ICBMClientErr,
  406. RequestID: inFrame.RequestID,
  407. },
  408. Body: wire.SNAC_0x04_0x0B_ICBMClientErr{
  409. Cookie: inBody.Cookie,
  410. ChannelID: inBody.ChannelID,
  411. ScreenName: instance.DisplayScreenName().String(),
  412. Code: inBody.Code,
  413. ErrInfo: inBody.ErrInfo,
  414. },
  415. })
  416. return nil
  417. }
  418. // EvilRequest handles user warning (a.k.a evil) notifications. It receives
  419. // wire.ICBMEvilRequest warning SNAC, increments the warned user's warning
  420. // level, and sends the warned user a notification informing them that they
  421. // have been warned. The user may choose to warn anonymously or
  422. // non-anonymously. It returns SNAC wire.ICBMEvilReply to confirm that the
  423. // warning was sent. Users may not warn themselves or warn users they have
  424. // blocked or are blocked by.
  425. func (s *ICBMService) EvilRequest(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x08_ICBMEvilRequest) (wire.SNACMessage, error) {
  426. identScreenName := state.NewIdentScreenName(inBody.ScreenName)
  427. // don't let users warn themselves, it causes the AIM client to go into a
  428. // weird state.
  429. if identScreenName == instance.IdentScreenName() {
  430. return *newICBMErr(inFrame.RequestID, wire.ErrorCodeNotSupportedByHost), nil
  431. }
  432. blocked, err := s.relationshipFetcher.Relationship(ctx, instance.IdentScreenName(), identScreenName)
  433. if err != nil {
  434. return wire.SNACMessage{}, err
  435. }
  436. if blocked.BlocksYou || blocked.YouBlock {
  437. // user or target is blocked
  438. return *newICBMErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  439. }
  440. recipSess := s.sessionRetriever.RetrieveSession(identScreenName)
  441. if recipSess == nil {
  442. // target user is offline
  443. return *newICBMErr(inFrame.RequestID, wire.ErrorCodeNotLoggedOn), nil
  444. }
  445. if recipSess.AllUserInfoBitmask(wire.OServiceUserFlagBot) {
  446. // target user is a bot, bots can't be warned
  447. return *newICBMErr(inFrame.RequestID, wire.ErrorCodeRequestDenied), nil
  448. }
  449. canWarn := s.convoTracker.trackWarn(time.Now(), instance.IdentScreenName(), recipSess.IdentScreenName())
  450. if !canWarn {
  451. // user has warned target too many times or not enough messages have
  452. // been received from target
  453. return *newICBMErr(inFrame.RequestID, wire.ErrorCodeRequestDenied), nil
  454. }
  455. increase := evilDelta
  456. if inBody.SendAs == 1 {
  457. increase = evilDeltaAnon
  458. }
  459. // get the rate class for sending IMs, which gets limited when the user gets warned
  460. classID, ok := s.snacRateLimits.RateClassLookup(wire.ICBM, wire.ICBMChannelMsgToHost)
  461. if !ok {
  462. panic("failed to retrieve rate class for ICBMChannelMsgToHost")
  463. }
  464. ok, newLevel := recipSess.ScaleWarningAndRateLimit(int16(increase), classID)
  465. if !ok {
  466. // target's warning is at 100%
  467. return *newICBMErr(inFrame.RequestID, wire.ErrorCodeRequestDenied), nil
  468. }
  469. notif := wire.SNAC_0x01_0x10_OServiceEvilNotification{
  470. NewEvil: newLevel,
  471. }
  472. // append info about user who sent the warning
  473. if inBody.SendAs == 0 {
  474. notif.Snitcher = &struct {
  475. wire.TLVUserInfo
  476. }{
  477. TLVUserInfo: wire.TLVUserInfo{
  478. ScreenName: instance.DisplayScreenName().String(),
  479. WarningLevel: instance.Warning(),
  480. },
  481. }
  482. }
  483. s.messageRelayer.RelayToScreenName(ctx, recipSess.IdentScreenName(), wire.SNACMessage{
  484. Frame: wire.SNACFrame{
  485. FoodGroup: wire.OService,
  486. SubGroup: wire.OServiceEvilNotification,
  487. },
  488. Body: notif,
  489. })
  490. return wire.SNACMessage{
  491. Frame: wire.SNACFrame{
  492. FoodGroup: wire.ICBM,
  493. SubGroup: wire.ICBMEvilReply,
  494. RequestID: inFrame.RequestID,
  495. },
  496. Body: wire.SNAC_0x04_0x09_ICBMEvilReply{
  497. EvilDeltaApplied: increase,
  498. UpdatedEvilValue: newLevel,
  499. },
  500. }, nil
  501. }
  502. func (s *ICBMService) OfflineRetrieve(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) (wire.SNACMessage, error) {
  503. msgList, err := s.offlineMessageManager.RetrieveMessages(ctx, instance.IdentScreenName())
  504. if err != nil {
  505. return wire.SNACMessage{}, fmt.Errorf("retrieving messages: %w", err)
  506. }
  507. for _, event := range msgList {
  508. clientIM := wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  509. Cookie: event.Message.Cookie,
  510. ChannelID: event.Message.ChannelID,
  511. TLVUserInfo: wire.TLVUserInfo{
  512. ScreenName: event.Sender.String(),
  513. },
  514. TLVRestBlock: wire.TLVRestBlock{},
  515. }
  516. for _, tlv := range event.Message.TLVList {
  517. // The stored SNAC is whatever the sender sent. A send time it carries
  518. // is not ours, and TLVList lookups return the first match, so it would
  519. // shadow the stamp appended below.
  520. if tlv.Tag == wire.ICBMTLVSendTime {
  521. continue
  522. }
  523. clientIM.Append(tlv)
  524. }
  525. clientIM.Append(wire.NewTLVBE(wire.ICBMTLVSendTime, uint32(event.Sent.Unix())))
  526. // Retrieval answers the instance that asked for it. Relaying to the screen
  527. // name would hand this reply to every other instance of the account, which
  528. // never requested it and cannot tell it apart from a live message.
  529. s.messageRelayer.RelayToSelf(ctx, instance, wire.SNACMessage{
  530. Frame: wire.SNACFrame{
  531. FoodGroup: wire.ICBM,
  532. SubGroup: wire.ICBMChannelMsgToClient,
  533. RequestID: wire.ReqIDFromServer,
  534. },
  535. Body: clientIM,
  536. })
  537. }
  538. if len(msgList) > 0 {
  539. if err := s.offlineMessageManager.DeleteMessages(ctx, instance.IdentScreenName()); err != nil {
  540. return wire.SNACMessage{}, fmt.Errorf("offlineMessageManager.DeleteMessages: %w", err)
  541. }
  542. }
  543. return wire.SNACMessage{
  544. Frame: wire.SNACFrame{
  545. FoodGroup: wire.ICBM,
  546. SubGroup: wire.ICBMOfflineRetrieveReply,
  547. RequestID: inFrame.RequestID,
  548. },
  549. Body: wire.SNAC_0x04_0x17_ICBMOfflineRetrieveReply{},
  550. }, nil
  551. }
  552. // RestoreWarningLevel restores the warning level from the last stored value at login time,
  553. // accounting for time passed between logins.
  554. func (s *ICBMService) RestoreWarningLevel(ctx context.Context, instance *state.SessionInstance) error {
  555. u, err := s.userManager.User(ctx, instance.IdentScreenName())
  556. if err != nil {
  557. return fmt.Errorf("failed to get user: %w", err)
  558. }
  559. if u == nil {
  560. return state.ErrNoUser
  561. }
  562. if u.LastWarnLevel == 0 {
  563. // user had no warning at the end of last session
  564. return nil
  565. }
  566. // get the rate class for sending IMs, which gets limited when the user gets warned
  567. classID, ok := s.snacRateLimits.RateClassLookup(wire.ICBM, wire.ICBMChannelMsgToHost)
  568. if !ok {
  569. panic("failed to retrieve rate class for ICBMChannelMsgToHost")
  570. }
  571. // increment warning level by the amount of time that has passed since last
  572. // login, proportionally increasing the warning level
  573. warnDelta := calcElapsedWarningLevel(u.LastWarnUpdate, s.timeNow(), s.interval)
  574. newWarning := int16(u.LastWarnLevel) + warnDelta
  575. instance.Session().SetWarning(0)
  576. instance.Session().ScaleWarningAndRateLimit(newWarning, classID)
  577. if instance.Warning() > 0 {
  578. s.logger.DebugContext(ctx, "restored warning level with time decay applied since last login",
  579. "stored_level", u.LastWarnLevel,
  580. "time_since_update", s.timeNow().Sub(u.LastWarnUpdate),
  581. "decay_delta", warnDelta,
  582. "final_level", instance.Warning(),
  583. )
  584. } else {
  585. s.logger.DebugContext(ctx, "warning level decayed to zero since last login",
  586. "stored_level", u.LastWarnLevel,
  587. "time_since_update", s.timeNow().Sub(u.LastWarnUpdate),
  588. "decay_delta", warnDelta,
  589. )
  590. }
  591. return nil
  592. }
  593. // UpdateWarnLevel periodically updates the warning level relative to time
  594. // elapsed between warnings.
  595. func (s *ICBMService) UpdateWarnLevel(ctx context.Context, instance *state.SessionInstance) {
  596. var inProgress bool
  597. var ticker *time.Ticker
  598. var tickC <-chan time.Time // nil when idle, enables/disables the select case
  599. var doReset bool
  600. stopTicker := func() {
  601. if ticker != nil {
  602. ticker.Stop()
  603. ticker = nil
  604. }
  605. tickC = nil
  606. inProgress = false
  607. s.logger.DebugContext(ctx, "warning decay stopped")
  608. }
  609. startTicker := func(interval time.Duration) {
  610. ticker = time.NewTicker(interval)
  611. tickC = ticker.C
  612. inProgress = true
  613. s.logger.DebugContext(ctx, "warning decay started")
  614. }
  615. if instance.Warning() > 0 {
  616. u, err := s.userManager.User(ctx, instance.IdentScreenName())
  617. if err != nil {
  618. s.logger.ErrorContext(ctx, "failed to get user", "err", err)
  619. return
  620. }
  621. newInterval := timeTillNextInterval(u.LastWarnUpdate, s.timeNow(), s.interval)
  622. interval := s.interval
  623. if newInterval > 0 {
  624. interval = newInterval
  625. }
  626. s.logger.DebugContext(ctx, "starting warning level update with interval adjusted to next boundary",
  627. "user", instance.IdentScreenName(),
  628. "adjusted_interval", interval,
  629. "default_interval", s.interval,
  630. "time_since_last_update", s.timeNow().Sub(u.LastWarnUpdate),
  631. )
  632. startTicker(interval)
  633. doReset = true
  634. }
  635. // get the rate class for sending IMs, which gets limited when the user gets warned
  636. classID, ok := s.snacRateLimits.RateClassLookup(wire.ICBM, wire.ICBMChannelMsgToHost)
  637. if !ok {
  638. panic("failed to retrieve rate class for ICBMChannelMsgToHost")
  639. }
  640. warnCh := make(chan struct{}, 1)
  641. var wg sync.WaitGroup
  642. wg.Add(1)
  643. go func() {
  644. defer wg.Done()
  645. defer close(warnCh)
  646. for {
  647. select {
  648. case <-instance.Closed():
  649. return
  650. case <-ctx.Done():
  651. return
  652. case warning := <-instance.WarningCh():
  653. if warning > 0 {
  654. warnCh <- struct{}{}
  655. }
  656. if err := s.userManager.SetWarnLevel(ctx, instance.IdentScreenName(), s.timeNow(), warning); err != nil {
  657. s.logger.ErrorContext(ctx, "failed to set warn level", "err", err)
  658. }
  659. info := instance.Session().TLVUserInfo()
  660. // lock in the current warning level to avoid race conditions
  661. // where the warning level might change during this broadcast
  662. // operation
  663. info.WarningLevel = warning
  664. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), info); err != nil {
  665. s.logger.ErrorContext(ctx, "BroadcastBuddyArrived failed", "err", err)
  666. } else {
  667. s.logger.DebugContext(ctx, "warning lowered", "remaining", warning)
  668. }
  669. }
  670. }
  671. }()
  672. defer wg.Wait()
  673. for {
  674. select {
  675. case <-instance.Closed():
  676. stopTicker()
  677. return
  678. case <-ctx.Done():
  679. stopTicker()
  680. return
  681. case <-warnCh:
  682. if inProgress {
  683. s.logger.DebugContext(ctx, "warning decay already in progress")
  684. continue
  685. }
  686. startTicker(s.interval)
  687. case <-tickC:
  688. if doReset {
  689. ticker.Reset(s.interval)
  690. doReset = false
  691. }
  692. ok, warning := instance.Session().ScaleWarningAndRateLimit(warningDecayPct, classID)
  693. if !ok {
  694. s.logger.ErrorContext(ctx, "warning increment out of rage", "level", warning)
  695. stopTicker()
  696. return
  697. }
  698. if warning == 0 {
  699. s.logger.DebugContext(ctx, "warning decay complete")
  700. stopTicker()
  701. }
  702. }
  703. }
  704. }
  705. func newICBMErr(requestID uint32, errCode uint16, tlvs ...wire.TLV) *wire.SNACMessage {
  706. body := wire.SNACError{
  707. Code: errCode,
  708. }
  709. if len(tlvs) > 0 {
  710. body.AppendList(tlvs)
  711. }
  712. return &wire.SNACMessage{
  713. Frame: wire.SNACFrame{
  714. FoodGroup: wire.ICBM,
  715. SubGroup: wire.ICBMErr,
  716. RequestID: requestID,
  717. },
  718. Body: body,
  719. }
  720. }
  721. func calcElapsedWarningLevel(lastWarnUpdate time.Time, now time.Time, interval time.Duration) int16 {
  722. // time passed since last signoff
  723. since := now.Sub(lastWarnUpdate)
  724. // how many times warning decayed since last signoff
  725. decayPeriods := int(since / interval)
  726. // total amount warning decreased since last signoff
  727. warnDelta := decayPeriods * warningDecayPct
  728. return int16(warnDelta)
  729. }
  730. func timeTillNextInterval(lastWarned time.Time, now time.Time, interval time.Duration) time.Duration {
  731. return interval - (now.Sub(lastWarned) % interval)
  732. }
  733. // convoTracker keeps track of messages initiated from a sender to a recipient.
  734. // A user (the warner) can only warn another user (the warnee) only if the
  735. // warner has received a message from the warnee. The warner may only warn 1
  736. // time per message received from warnee. The warner may only warn the warnee
  737. // up to 3 times per warn window.
  738. type convoTracker struct {
  739. convos *cache.Cache
  740. warns *cache.Cache
  741. window time.Duration
  742. }
  743. func newConvoTracker() *convoTracker {
  744. window := 1 * time.Hour
  745. return &convoTracker{
  746. convos: cache.New(window, window),
  747. warns: cache.New(window, window),
  748. window: window,
  749. }
  750. }
  751. // trackConvo records a conversation from sender to recipient at the given time.
  752. func (w *convoTracker) trackConvo(now time.Time, sender, recip state.IdentScreenName) {
  753. k := w.key(sender, recip)
  754. buf, found := w.convos.Get(k)
  755. if !found {
  756. buf = &ringBuffer{}
  757. w.convos.Set(k, buf, time.Hour)
  758. }
  759. buf.(*ringBuffer).set(now)
  760. }
  761. // trackWarn attempts to record a warning from warner to warnee.
  762. // It returns true if the warning is allowed (warnee has sent more messages
  763. // than warnings in the current window), or false if the warning limit has been
  764. // reached or no conversation exists in the current window.
  765. func (w *convoTracker) trackWarn(now time.Time, warner, warnee state.IdentScreenName) bool {
  766. key := w.key(warnee, warner)
  767. convos, found := w.convos.Get(key)
  768. if !found {
  769. // no convos tracked, can't warn
  770. return false
  771. }
  772. windowStart := now.Add(-w.window)
  773. // get convo count during window
  774. var convoCt int
  775. for _, v := range convos.(*ringBuffer).vals {
  776. if v.After(windowStart) {
  777. convoCt++
  778. }
  779. }
  780. warns, found := w.warns.Get(key)
  781. if !found {
  782. warns = &ringBuffer{}
  783. w.warns.Set(key, warns, time.Hour)
  784. }
  785. // get warn count during window
  786. var warnCount int
  787. for _, v := range warns.(*ringBuffer).vals {
  788. if v.After(windowStart) {
  789. warnCount++
  790. }
  791. }
  792. if convoCt <= warnCount {
  793. return false
  794. }
  795. warns.(*ringBuffer).set(now)
  796. return true
  797. }
  798. func (w *convoTracker) key(sender state.IdentScreenName, recip state.IdentScreenName) string {
  799. return sender.String() + recip.String()
  800. }
  801. // ringBuffer is a fixed-size circular buffer with 3 slots for storing time values.
  802. type ringBuffer struct {
  803. cur int // Current cursor position (0, 1, or 2)
  804. vals [3]time.Time // Fixed-size array to store time values
  805. }
  806. // val returns the time at the current cursor position.
  807. func (r *ringBuffer) val() time.Time {
  808. return r.vals[r.cur]
  809. }
  810. // set stores the given time at the current cursor position and advances the cursor.
  811. func (r *ringBuffer) set(v time.Time) {
  812. r.vals[r.cur] = v
  813. r.cur = (r.cur + 1) % len(r.vals)
  814. }