icbm.go 28 KB

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