4
0

cmd_server.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. package toc
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "net"
  10. "strings"
  11. "github.com/google/uuid"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. var (
  15. // cmdInternalSvcErr indicates a general failure. Use wire.TOCErrorAdminProcessingRequest
  16. // error code as this is the closest applicable code as per TiK, Tameclone, phptoclib.
  17. cmdInternalSvcErr = "ERROR:" + wire.TOCErrorAdminProcessingRequest + ":internal server error"
  18. rateLimitExceededErr = "ERROR:" + wire.TOCErrorGeneralRateLimitHit
  19. errDisconnect = errors.New("got booted by another session")
  20. )
  21. // OServiceRateParamsChange codes the OSCAR server sends when one of the user's
  22. // rate classes becomes limited or clears.
  23. const (
  24. rateParamsChangeLimited uint16 = 3
  25. rateParamsChangeClear uint16 = 4
  26. )
  27. // RecvBOS routes SNACs from the BOS server, replies and server-initiated
  28. // messages alike, to their corresponding TOC handlers. It ignores any SNAC for
  29. // which there is no TOC response.
  30. func (s OSCARProxy) RecvBOS(ctx context.Context, me *tocConn) error {
  31. for _, snac := range me.backlog {
  32. s.recvBOSSNAC(ctx, me, snac)
  33. }
  34. me.backlog = nil
  35. for {
  36. select {
  37. case <-ctx.Done():
  38. me.bos.Close()
  39. return nil
  40. case <-me.bos.Closed():
  41. return errDisconnect
  42. case snac := <-me.bos.Messages():
  43. s.recvBOSSNAC(ctx, me, snac)
  44. }
  45. }
  46. }
  47. func (s OSCARProxy) recvBOSSNAC(ctx context.Context, me *tocConn, snac wire.SNACMessage) {
  48. var msg []string
  49. switch v := snac.Body.(type) {
  50. case wire.SNAC_0x03_0x0B_BuddyArrived:
  51. msg = s.UpdateBuddyArrival(v, me)
  52. case wire.SNAC_0x03_0x0C_BuddyDeparted:
  53. msg = s.UpdateBuddyDeparted(v, me)
  54. case wire.SNAC_0x04_0x07_ICBMChannelMsgToClient:
  55. msg = s.IMIn(ctx, me, v)
  56. case wire.SNAC_0x01_0x10_OServiceEvilNotification:
  57. msg = s.Eviled(v)
  58. case wire.SNAC_0x01_0x0A_OServiceRateParamsChange:
  59. msg = s.RateParamsChange(v)
  60. if v.Code == rateParamsChangeClear && me.feedbag.hasPending() {
  61. // the server may have dropped feedbag changes without replying
  62. // while limited
  63. msg = s.reloadFeedbag(ctx, me)
  64. }
  65. case wire.SNAC_0x04_0x14_ICBMClientEvent:
  66. if me.IsTOC2() {
  67. msg = s.ClientEvent(v)
  68. }
  69. case wire.SNAC_0x13_0x08_FeedbagInsertItem:
  70. if me.IsTOC2() {
  71. me.feedbag.applyRemote(false, v.Items)
  72. msg = s.Inserted2(me, v.Items)
  73. }
  74. case wire.SNAC_0x13_0x09_FeedbagUpdateItem:
  75. if me.IsTOC2() {
  76. me.feedbag.applyRemote(false, v.Items)
  77. msg = s.Inserted2(me, v.Items)
  78. }
  79. case wire.SNAC_0x13_0x0A_FeedbagDeleteItem:
  80. if me.IsTOC2() {
  81. msg = s.Deleted2(me, v)
  82. me.feedbag.applyRemote(true, v.Items)
  83. }
  84. case wire.SNACError:
  85. msg = s.SNACError(ctx, me, snac.Frame, v)
  86. case wire.SNAC_0x02_0x06_LocateUserInfoReply:
  87. msg = s.UserInfoReply(me, snac.Frame, v)
  88. case wire.SNAC_0x07_0x05_AdminChangeReply:
  89. msg = s.AdminChangeReply(ctx, me, snac.Frame, v)
  90. case wire.SNAC_0x0D_0x09_ChatNavNavInfo:
  91. msg = s.ChatNavInfo(ctx, me, snac.Frame, v)
  92. case wire.SNAC_0x01_0x05_OServiceServiceResponse:
  93. msg = s.ChatServiceResponse(ctx, me, snac.Frame, v)
  94. case wire.SNAC_0x13_0x0E_FeedbagStatus:
  95. s.FeedbagStatus(me, snac.Frame, v)
  96. case wire.SNAC_0x13_0x06_FeedbagReply:
  97. s.FeedbagReply(me, snac.Frame, v)
  98. case wire.SNAC_0x04_0x09_ICBMEvilReply:
  99. // the warning succeeded and there's nothing to tell the client
  100. me.cache.take(snac.Frame)
  101. default:
  102. s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s",
  103. wire.FoodGroupName(snac.Frame.FoodGroup),
  104. wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup)))
  105. }
  106. if len(msg) > 0 {
  107. sendOrCancel(ctx, me.toCh, msg)
  108. }
  109. }
  110. // RecvChat routes SNACs from a chat room connection to their corresponding
  111. // TOC handlers. It ignores any SNAC for which there is no TOC response.
  112. func (s OSCARProxy) RecvChat(ctx context.Context, me *tocConn, conn oscarConn, chatID int) {
  113. for {
  114. select {
  115. case <-ctx.Done():
  116. conn.Close()
  117. return
  118. case <-conn.Closed():
  119. return
  120. case snac := <-conn.Messages():
  121. switch v := snac.Body.(type) {
  122. case wire.SNAC_0x0E_0x04_ChatUsersLeft:
  123. sendOrCancel(ctx, me.toCh, s.ChatUpdateBuddyLeft(v, chatID))
  124. case wire.SNAC_0x0E_0x03_ChatUsersJoined:
  125. sendOrCancel(ctx, me.toCh, s.ChatUpdateBuddyArrived(v, chatID))
  126. case wire.SNAC_0x0E_0x06_ChatChannelMsgToClient:
  127. // a reflected message answers the user's own toc_chat_send
  128. me.cache.take(snac.Frame)
  129. sendOrCancel(ctx, me.toCh, s.ChatIn(ctx, me, v, chatID))
  130. case wire.SNAC_0x01_0x0A_OServiceRateParamsChange:
  131. if msg := s.RateParamsChange(v); len(msg) > 0 {
  132. sendOrCancel(ctx, me.toCh, msg)
  133. }
  134. case wire.SNACError:
  135. if msg := s.SNACError(ctx, me, snac.Frame, v); len(msg) > 0 {
  136. sendOrCancel(ctx, me.toCh, msg)
  137. }
  138. default:
  139. s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s",
  140. wire.FoodGroupName(snac.Frame.FoodGroup),
  141. wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup)))
  142. }
  143. }
  144. }
  145. }
  146. // RateParamsChange tells the client that it's being rate limited when one of
  147. // its rate classes becomes limited, since the OSCAR server drops rate-limited
  148. // requests without replying.
  149. func (s OSCARProxy) RateParamsChange(snac wire.SNAC_0x01_0x0A_OServiceRateParamsChange) []string {
  150. if snac.Code == rateParamsChangeLimited {
  151. return []string{rateLimitExceededErr}
  152. }
  153. return nil
  154. }
  155. // SNACError handles an error reply from the OSCAR server according to the
  156. // request that caused it.
  157. func (s OSCARProxy) SNACError(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNACError) []string {
  158. req, ok := me.cache.take(frame)
  159. if !ok {
  160. s.Logger.DebugContext(ctx, "received SNAC error for unknown request",
  161. "foodgroup", wire.FoodGroupName(frame.FoodGroup), "code", snac.Code)
  162. return nil
  163. }
  164. switch data := req.data.(type) {
  165. case getStatusReqCtx:
  166. if snac.Code == wire.ErrorCodeNotLoggedOn {
  167. return []string{fmt.Sprintf("ERROR:%s:%s", wire.TOCErrorGeneralUserNotAvailable, data.screenName)}
  168. }
  169. return s.runtimeErr(ctx, fmt.Errorf("LocateUserInfoQuery error code: %d", snac.Code))
  170. case evilReqCtx:
  171. s.Logger.InfoContext(ctx, "unable to warn user", "code", snac.Code)
  172. return []string{"ERROR:" + wire.TOCErrorGeneralWarningUserNotAvailable}
  173. case feedbagChangeReqCtx:
  174. s.Logger.DebugContext(ctx, "feedbag change failed", "code", snac.Code)
  175. me.feedbag.discard(frame.RequestID)
  176. return nil
  177. case chatJoinReqCtx, chatAcceptReqCtx, chatServiceReqCtx:
  178. return s.runtimeErr(ctx, fmt.Errorf("unable to join chat room, %s error code: %d",
  179. wire.SubGroupName(req.frame.FoodGroup, req.frame.SubGroup), snac.Code))
  180. default:
  181. s.Logger.DebugContext(ctx, "received SNAC error",
  182. "request", wire.SubGroupName(req.frame.FoodGroup, req.frame.SubGroup), "code", snac.Code)
  183. return nil
  184. }
  185. }
  186. // FeedbagStatus confirms the items of a feedbag change that the server
  187. // accepted.
  188. func (s OSCARProxy) FeedbagStatus(me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x13_0x0E_FeedbagStatus) {
  189. me.cache.take(frame)
  190. me.feedbag.confirm(frame.RequestID, snac.Results)
  191. }
  192. // FeedbagReply finishes reloading the feedbag mirror.
  193. func (s OSCARProxy) FeedbagReply(me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x13_0x06_FeedbagReply) {
  194. req, ok := me.cache.take(frame)
  195. if !ok {
  196. return
  197. }
  198. if data, ok := req.data.(feedbagReloadReqCtx); ok {
  199. me.feedbag.reload(data.marker, snac.Items)
  200. }
  201. }
  202. // UserInfoReply handles the user info reply to toc_get_status, which is sent
  203. // to the client as UPDATE_BUDDY.
  204. func (s OSCARProxy) UserInfoReply(me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x02_0x06_LocateUserInfoReply) []string {
  205. me.cache.take(frame)
  206. return []string{userInfoToUpdateBuddy(snac.TLVUserInfo, me)}
  207. }
  208. // AdminChangeReply handles the reply to toc_change_passwd, sent as
  209. // ADMIN_PASSWD_STATUS, or to toc_format_nickname, sent as ADMIN_NICK_STATUS
  210. // and NICK.
  211. func (s OSCARProxy) AdminChangeReply(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x07_0x05_AdminChangeReply) []string {
  212. req, ok := me.cache.take(frame)
  213. if !ok {
  214. s.Logger.DebugContext(ctx, "received admin change reply for unknown request")
  215. return nil
  216. }
  217. code, hasErr := snac.Uint16BE(wire.AdminTLVErrorCode)
  218. switch req.data.(type) {
  219. case changePasswdReqCtx:
  220. if hasErr {
  221. switch code {
  222. case wire.AdminInfoErrorInvalidPasswordLength:
  223. return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
  224. case wire.AdminInfoErrorValidatePassword:
  225. return []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword}
  226. default:
  227. return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
  228. }
  229. }
  230. return []string{"ADMIN_PASSWD_STATUS:0"}
  231. case formatNickReqCtx:
  232. if hasErr {
  233. switch code {
  234. case wire.AdminInfoErrorInvalidNickNameLength, wire.AdminInfoErrorInvalidNickName:
  235. return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
  236. default:
  237. return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
  238. }
  239. }
  240. val, hasVal := snac.String(wire.AdminTLVScreenNameFormatted)
  241. if !hasVal {
  242. return s.runtimeErr(ctx, fmt.Errorf("AdminChangeReply: missing AdminTLVScreenNameFormatted %v", snac))
  243. }
  244. return []string{"ADMIN_NICK_STATUS:0", "NICK:" + val}
  245. default:
  246. return s.runtimeErr(ctx, fmt.Errorf("AdminChangeReply: unexpected request %v", req.frame))
  247. }
  248. }
  249. // ChatNavInfo continues toc_chat_join and toc_chat_accept once the chat room
  250. // info arrives, by requesting a connection to the room.
  251. func (s OSCARProxy) ChatNavInfo(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x0D_0x09_ChatNavNavInfo) []string {
  252. req, ok := me.cache.take(frame)
  253. if !ok {
  254. s.Logger.DebugContext(ctx, "received chat nav info for unknown request")
  255. return nil
  256. }
  257. b, hasInfo := snac.Bytes(wire.ChatNavTLVRoomInfo)
  258. if !hasInfo {
  259. return s.runtimeErr(ctx, errors.New("ChatNavNavInfo: missing wire.ChatNavTLVRoomInfo"))
  260. }
  261. roomInfo := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
  262. if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(b)); err != nil {
  263. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  264. }
  265. roomName, hasName := roomInfo.String(wire.ChatRoomTLVRoomName)
  266. if !hasName {
  267. return s.runtimeErr(ctx, errors.New("roomInfo.String: missing wire.ChatRoomTLVRoomName"))
  268. }
  269. switch data := req.data.(type) {
  270. case chatJoinReqCtx:
  271. chatID := me.chats.Add(wire.ICBMRoomInfo{
  272. Exchange: roomInfo.Exchange,
  273. Cookie: roomInfo.Cookie,
  274. Instance: roomInfo.InstanceNumber,
  275. })
  276. return s.requestChatService(ctx, me, chatID, roomName, roomInfo.Cookie)
  277. case chatAcceptReqCtx:
  278. return s.requestChatService(ctx, me, data.chatID, roomName, roomInfo.Cookie)
  279. default:
  280. return s.runtimeErr(ctx, fmt.Errorf("ChatNavNavInfo: unexpected request %v", req.frame))
  281. }
  282. }
  283. // ChatServiceResponse finishes toc_chat_join and toc_chat_accept by
  284. // connecting to the chat room. It sends CHAT_JOIN before relaying anything
  285. // from the room, so that the client knows the room before its first
  286. // CHAT_UPDATE_BUDDY.
  287. func (s OSCARProxy) ChatServiceResponse(ctx context.Context, me *tocConn, frame wire.SNACFrame, snac wire.SNAC_0x01_0x05_OServiceServiceResponse) []string {
  288. req, ok := me.cache.take(frame)
  289. if !ok {
  290. s.Logger.DebugContext(ctx, "received service response for unknown request")
  291. return nil
  292. }
  293. data, ok := req.data.(chatServiceReqCtx)
  294. if !ok {
  295. return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceResponse: unexpected request %v", req.frame))
  296. }
  297. loginCookie, hasCookie := snac.Bytes(wire.OServiceTLVTagsLoginCookie)
  298. if !hasCookie {
  299. return s.runtimeErr(ctx, errors.New("OServiceServiceResponse: missing wire.OServiceTLVTagsLoginCookie"))
  300. }
  301. conn, err := s.connectChat(ctx, me, loginCookie)
  302. if err != nil {
  303. return s.runtimeErr(ctx, fmt.Errorf("connectChat: %w", err))
  304. }
  305. me.chats.RegisterConn(data.chatID, conn)
  306. sendOrCancel(ctx, me.toCh, []string{fmt.Sprintf("CHAT_JOIN:%d:%s", data.chatID, data.roomName)})
  307. me.goAsync(func() error {
  308. s.RecvChat(ctx, me, conn, data.chatID)
  309. return nil
  310. })
  311. return nil
  312. }
  313. // ChatIn handles the CHAT_IN and ENC_CHAT_IN TOC commands.
  314. //
  315. // From the TiK documentation:
  316. //
  317. // A chat message was sent in a chat room.
  318. //
  319. // From the BlueTOC documentation:
  320. //
  321. // This command received instead of CHAT_IN. It is similar to TOC 1.0 except there are a two new parameters.
  322. // One of them is language; the other is unknown but is usually "A"
  323. //
  324. // Command syntax: CHAT_IN:<Chat Room Id>:<Source User>:<Whisper? T/F>:<Message>
  325. // Command syntax: CHAT_IN_ENC:<chatroom id>:<user>:<whisper T/F>:<???>:en:<message>
  326. func (s OSCARProxy) ChatIn(ctx context.Context, me *tocConn, snac wire.SNAC_0x0E_0x06_ChatChannelMsgToClient, chatID int) []string {
  327. b, ok := snac.Bytes(wire.ChatTLVSenderInformation)
  328. if !ok {
  329. return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVSenderInformation"))
  330. }
  331. u := wire.TLVUserInfo{}
  332. err := wire.UnmarshalBE(&u, bytes.NewReader(b))
  333. if err != nil {
  334. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  335. }
  336. b, ok = snac.Bytes(wire.ChatTLVMessageInfo)
  337. if !ok {
  338. return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVMessageInfo"))
  339. }
  340. text, err := wire.UnmarshalChatMessageText(b)
  341. if err != nil {
  342. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalChatMessageText: %w", err))
  343. }
  344. if me.SupportsTOC2MsgEnc() {
  345. return []string{fmt.Sprintf("CHAT_IN_ENC:%d:%s:F:A:en:%s", chatID, u.ScreenName, text)}
  346. }
  347. return []string{fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, u.ScreenName, text)}
  348. }
  349. // ChatUpdateBuddyArrived handles the CHAT_UPDATE_BUDDY TOC command for chat
  350. // room arrival events.
  351. //
  352. // From the TiK documentation:
  353. //
  354. // This one command handles arrival/departs from a chat room. The very first
  355. // message of this type for each chat room contains the users already in the
  356. // room.
  357. //
  358. // Command syntax: CHAT_UPDATE_BUDDY:<Chat Room Id>:<Inside? T/F>:<User 1>:<User 2>...
  359. func (s OSCARProxy) ChatUpdateBuddyArrived(snac wire.SNAC_0x0E_0x03_ChatUsersJoined, chatID int) []string {
  360. users := make([]string, 0, len(snac.Users))
  361. for _, u := range snac.Users {
  362. users = append(users, u.ScreenName)
  363. }
  364. return []string{fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:T:%s", chatID, strings.Join(users, ":"))}
  365. }
  366. // ChatUpdateBuddyLeft handles the CHAT_UPDATE_BUDDY TOC command for chat
  367. // room departure events.
  368. //
  369. // From the TiK documentation:
  370. //
  371. // This one command handles arrival/departs from a chat room. The very first
  372. // message of this type for each chat room contains the users already in the
  373. // room.
  374. //
  375. // Command syntax: CHAT_UPDATE_BUDDY:<Chat Room Id>:<Inside? T/F>:<User 1>:<User 2>...
  376. func (s OSCARProxy) ChatUpdateBuddyLeft(snac wire.SNAC_0x0E_0x04_ChatUsersLeft, chatID int) []string {
  377. users := make([]string, 0, len(snac.Users))
  378. for _, u := range snac.Users {
  379. users = append(users, u.ScreenName)
  380. }
  381. return []string{fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:F:%s", chatID, strings.Join(users, ":"))}
  382. }
  383. // Eviled handles the EVILED TOC command.
  384. //
  385. // From the TiK documentation:
  386. //
  387. // The user was just eviled.
  388. //
  389. // Command syntax: EVILED:<new evil>:<name of eviler, blank if anonymous>
  390. func (s OSCARProxy) Eviled(snac wire.SNAC_0x01_0x10_OServiceEvilNotification) []string {
  391. warning := fmt.Sprintf("%d", snac.NewEvil/10)
  392. who := ""
  393. if snac.Snitcher != nil {
  394. who = snac.Snitcher.ScreenName
  395. }
  396. return []string{fmt.Sprintf("EVILED:%s:%s", warning, who)}
  397. }
  398. // IMIn handles incoming ICBM channel messages and returns one of: IM_IN (TOC1), IM_IN2 or
  399. // IM_IN_ENC2 (TOC2), or for rendezvous channel CHAT_INVITE or RVOUS_PROPOSE.
  400. //
  401. // From the TiK documentation (TOC1 IM_IN):
  402. //
  403. // Receive an IM from someone. Everything after the third colon is the
  404. // incoming message, including other colons.
  405. //
  406. // TOC2 clients receive IM_IN2 (same structure as IM_IN with an extra field) or
  407. // IM_IN_ENC2 when the client supports encoded messages (BlueTOC/BizTOCSock documentation).
  408. // For ICBM rendezvous (chat invite, file transfer), this returns CHAT_INVITE or RVOUS_PROPOSE
  409. // instead (see convertICBMRendezvous).
  410. //
  411. // Command syntax: IM_IN:<Source User>:<Auto Response T/F?>:<Message>
  412. // Command syntax: IM_IN2:<Source User>:<Auto Response T/F?>:<Whisper?>:<Message>
  413. // Command syntax: IM_IN_ENC2:<User>:<Auto>:<???>:<???>:<User Class>:<???>:<???>:<Language>:<Message>
  414. func (s OSCARProxy) IMIn(ctx context.Context, me *tocConn, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) []string {
  415. switch snac.ChannelID {
  416. case wire.ICBMChannelIM:
  417. return []string{s.convertICBMInstantMsg(ctx, me, snac)}
  418. case wire.ICBMChannelRendezvous:
  419. return []string{s.convertICBMRendezvous(ctx, me.chats, snac)}
  420. default:
  421. s.Logger.DebugContext(ctx, "received unsupported ICBM channel message", "channel_id", snac.ChannelID)
  422. return []string{}
  423. }
  424. }
  425. // convertICBMInstantMsg converts an ICBM instant message SNAC to a TOC IM_IN or TOC2 IM_IN2, or TOC2Enhanced IM_IN_ENC2 response.
  426. func (s OSCARProxy) convertICBMInstantMsg(ctx context.Context, me *tocConn, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
  427. buf, ok := snac.Bytes(wire.ICBMTLVAOLIMData)
  428. if !ok {
  429. return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing wire.ICBMTLVAOLIMData"))[0]
  430. }
  431. txt, err := wire.UnmarshalICBMMessageText(buf)
  432. if err != nil {
  433. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalICBMMessageText: %w", err))[0]
  434. }
  435. autoResp := "F"
  436. if _, isAutoReply := snac.Bytes(wire.ICBMTLVAutoResponse); isAutoReply {
  437. autoResp = "T"
  438. }
  439. if me.SupportsTOC2MsgEnc() {
  440. uFlags, _ := snac.TLVUserInfo.Uint16BE(wire.OServiceUserInfoUserFlags)
  441. classStr := userClassString(uFlags, snac.IsAway())
  442. // from a packet dump found in this russian zine: https://xn--lcss68aj21b.xn--w8je.xn--tckwe/books/xakep/spec65.pdf
  443. // interesting that "L" is a value, not sure what it's for.
  444. return fmt.Sprintf("IM_IN_ENC2:%s:%s:F:T:%s:F:L:en:%s", snac.ScreenName, autoResp, classStr, txt)
  445. }
  446. if me.IsTOC2() {
  447. return fmt.Sprintf("IM_IN2:%s:%s:%s:%s", snac.ScreenName, autoResp, "F", txt)
  448. }
  449. return fmt.Sprintf("IM_IN:%s:%s:%s", snac.ScreenName, autoResp, txt)
  450. }
  451. // convertICBMRendezvous converts an ICBM rendezvous SNAC to a TOC response.
  452. // - if chat, return CHAT_INVITE
  453. // - file transfer, return RVOUS_PROPOSE
  454. // - don't respond for other rendezvous types
  455. func (s OSCARProxy) convertICBMRendezvous(ctx context.Context, chatRegistry *ChatRegistry, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
  456. rdinfo, has := snac.Bytes(wire.ICBMTLVData)
  457. if !has {
  458. return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing rendezvous block"))[0]
  459. }
  460. frag := wire.ICBMCh2Fragment{}
  461. if err := wire.UnmarshalBE(&frag, bytes.NewReader(rdinfo)); err != nil {
  462. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))[0]
  463. }
  464. if frag.Type != wire.ICBMRdvMessagePropose {
  465. s.Logger.DebugContext(ctx, "can't convert ICBM rendezvous message to TOC response", "rdv_type", frag.Type)
  466. return ""
  467. }
  468. switch uuid.UUID(frag.Capability) {
  469. case wire.CapChat:
  470. prompt, ok := frag.Bytes(wire.ICBMRdvTLVTagsInvitation)
  471. if !ok {
  472. return s.runtimeErr(ctx, errors.New("frag.Bytes: missing chat invite prompt"))[0]
  473. }
  474. svcData, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData)
  475. if !ok || svcData == nil {
  476. return s.runtimeErr(ctx, errors.New("frag.Bytes: missing room info"))[0]
  477. }
  478. roomInfo := wire.ICBMRoomInfo{}
  479. if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(svcData)); err != nil {
  480. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))[0]
  481. }
  482. cookie := strings.Split(roomInfo.Cookie, "-") // make this safe
  483. if len(cookie) < 3 {
  484. return s.runtimeErr(ctx, errors.New("roomInfo.Cookie: malformed cookie, could not get room name"))[0]
  485. }
  486. roomName := cookie[2]
  487. chatID := chatRegistry.Add(roomInfo)
  488. return fmt.Sprintf("CHAT_INVITE:%s:%d:%s:%s", roomName, chatID, snac.ScreenName, prompt)
  489. case wire.CapFileTransfer:
  490. user := snac.ScreenName
  491. capability := strings.ToUpper(wire.CapFileTransfer.String()) // TiK requires upper-case UUID characters
  492. cookie := base64.StdEncoding.EncodeToString(frag.Cookie[:])
  493. seq, _ := frag.Uint16BE(wire.ICBMRdvTLVTagsSeqNum)
  494. rvousIP := "0.0.0.0"
  495. if ip, ok := frag.Bytes(wire.ICBMRdvTLVTagsRdvIP); ok && len(ip) == 4 {
  496. rvousIP = net.IPv4(ip[0], ip[1], ip[2], ip[3]).String()
  497. }
  498. proposerIP := "0.0.0.0"
  499. if ip, ok := frag.Bytes(wire.ICBMRdvTLVTagsRequesterIP); ok && len(ip) == 4 {
  500. proposerIP = net.IPv4(ip[0], ip[1], ip[2], ip[3]).String()
  501. }
  502. verifiedIP := "0.0.0.0"
  503. if ip, ok := frag.Bytes(wire.ICBMRdvTLVTagsVerifiedIP); ok && len(ip) == 4 {
  504. verifiedIP = net.IPv4(ip[0], ip[1], ip[2], ip[3]).String()
  505. }
  506. rvousPort, _ := frag.Uint16BE(wire.ICBMRdvTLVTagsPort)
  507. var fileMetadata string
  508. if f, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData); ok {
  509. // remove sequence of null bytes from the end that causes TiK file open
  510. // dialog to crash
  511. f = bytes.TrimRight(f, "\x00")
  512. fileMetadata = base64.StdEncoding.EncodeToString(f)
  513. }
  514. return fmt.Sprintf("RVOUS_PROPOSE:%s:%s:%s:%d:%s:%s:%s:%d:%d:%s",
  515. user, capability, cookie, seq, rvousIP, proposerIP, verifiedIP, rvousPort, wire.ICBMRdvTLVTagsSvcData, fileMetadata)
  516. default:
  517. s.Logger.DebugContext(ctx, "received rendezvous ICBM for unsupported capability", "capability", wire.CapChat)
  518. return ""
  519. }
  520. }
  521. // UpdateBuddyArrival handles the UPDATE_BUDDY TOC command for buddy arrival events.
  522. //
  523. // From the TiK documentation:
  524. //
  525. // This one command handles arrival/depart/updates. Evil Amount is a percentage, Signon Time is UNIX epoc, idle time is in minutes, UC (User Class) is a two/three character string.
  526. // - uc[0]
  527. // - ' ' - Ignore
  528. // - 'A' - On AOL
  529. // - uc[1]
  530. // - ' ' - Ignore
  531. // - 'A' - Oscar Admin
  532. // - 'U' - Oscar Unconfirmed
  533. // - 'O' - Oscar Normal
  534. // - uc[2]
  535. // - '\0' - Ignore
  536. // - ' ' - Ignore
  537. // - 'U' - The user has set their unavailable flag.
  538. //
  539. // Command syntax: UPDATE_BUDDY:<Buddy User>:<Online? T/F>:<Evil Amount>:<Signon Time>:<IdleTime>:<UC>
  540. //
  541. // For TOC2 this sends UPDATE_BUDDY2 with the same fields (plus a trailing field). When
  542. // the buddy has capabilities, BUDDY_CAPS2 is also sent (see userInfoToBuddyCaps).
  543. func (s OSCARProxy) UpdateBuddyArrival(snac wire.SNAC_0x03_0x0B_BuddyArrived, me *tocConn) []string {
  544. msgs := []string{userInfoToUpdateBuddy(snac.TLVUserInfo, me)}
  545. if caps := userInfoToBuddyCaps(snac.TLVUserInfo, me, s.Logger); caps != "" {
  546. msgs = append(msgs, caps)
  547. }
  548. return msgs
  549. }
  550. // UpdateBuddyDeparted handles the UPDATE_BUDDY TOC command for buddy departure events.
  551. //
  552. // From the TiK documentation:
  553. //
  554. // This one command handles arrival/depart/updates. Evil Amount is a
  555. // percentage, Signon Time is UNIX epoc, idle time is in minutes, UC (User
  556. // Class) is a two/three character string.
  557. // - uc[0]
  558. // - ' ' - Ignore
  559. // - 'A' - On AOL
  560. // - uc[1]
  561. // - ' ' - Ignore
  562. // - 'A' - Oscar Admin
  563. // - 'U' - Oscar Unconfirmed
  564. // - 'O' - Oscar Normal
  565. // - uc[2]
  566. // - '\0' - Ignore
  567. // - ' ' - Ignore
  568. // - 'U' - The user has set their unavailable flag.
  569. //
  570. // Command syntax: UPDATE_BUDDY:<Buddy User>:<Online? T/F>:<Evil Amount>:<Signon Time>:<IdleTime>:<UC>
  571. // TOC2 uses UPDATE_BUDDY2 with the same fields.
  572. func (s OSCARProxy) UpdateBuddyDeparted(snac wire.SNAC_0x03_0x0C_BuddyDeparted, me *tocConn) []string {
  573. if me.IsTOC2() {
  574. return []string{fmt.Sprintf("UPDATE_BUDDY2:%s:F:0:0:0: :", snac.ScreenName)}
  575. }
  576. return []string{fmt.Sprintf("UPDATE_BUDDY:%s:F:0:0:0: ", snac.ScreenName)}
  577. }
  578. // Inserted2 handles the INSERTED2 TOC2 server-to-client notifications.
  579. //
  580. // From the BlueTOC documentation:
  581. //
  582. // Sent whenever the buddy list is modified from a different location (e.g. logged
  583. // in twice). Dynamic updates when items are added to the buddy list.
  584. //
  585. // INSERTED2:g:<group name>
  586. // A new group has been added to the buddy list.
  587. //
  588. // INSERTED2:b:<alias>:<username>:<group>
  589. // A new screenname has been added.
  590. //
  591. // INSERTED2:d:<username>
  592. // Somebody has been added to the deny list.
  593. //
  594. // INSERTED2:p:<username>
  595. // Somebody has been added to the permit list.
  596. //
  597. // Inserted2 is invoked when this session receives FeedbagInsertItem or
  598. // FeedbagUpdateItem (e.g. list modified from another client). Group names come
  599. // from the feedbag mirror; buddy alias comes from TLV.
  600. func (s OSCARProxy) Inserted2(me *tocConn, items []wire.FeedbagItem) []string {
  601. var out []string
  602. groupNameByID := me.feedbag.groupNames()
  603. for _, item := range items {
  604. switch item.ClassID {
  605. case wire.FeedbagClassIdGroup:
  606. out = append(out, fmt.Sprintf("INSERTED2:g:%s", item.Name))
  607. case wire.FeedbagClassIdBuddy:
  608. group := groupNameByID[item.GroupID]
  609. if group == "" {
  610. group = "Buddies"
  611. }
  612. alias := ""
  613. if b, ok := item.Bytes(wire.FeedbagAttributesAlias); ok {
  614. alias = string(b)
  615. }
  616. out = append(out, fmt.Sprintf("INSERTED2:b:%s:%s:%s", alias, item.Name, group))
  617. case wire.FeedbagClassIDDeny:
  618. out = append(out, fmt.Sprintf("INSERTED2:d:%s", item.Name))
  619. case wire.FeedbagClassIDPermit:
  620. out = append(out, fmt.Sprintf("INSERTED2:p:%s", item.Name))
  621. }
  622. }
  623. return out
  624. }
  625. // Deleted2 handles the DELETED2 TOC2 server-to-client notifications.
  626. //
  627. // From the BlueTOC documentation:
  628. //
  629. // Sent whenever the buddy list is modified from a different location. Dynamic
  630. // updates when items are removed from the buddy list.
  631. //
  632. // DELETED2:g:<group name>
  633. // A group has been deleted from the buddy list.
  634. //
  635. // DELETED2:b:<username>:<group>
  636. // A user has been deleted from the buddy list.
  637. //
  638. // DELETED2:d:<username>
  639. // A user has been removed from the deny list.
  640. //
  641. // DELETED2:p:<username>
  642. // A user has been removed from the permit list.
  643. //
  644. // Deleted2 is invoked when this session receives FeedbagDeleteItem (e.g. list
  645. // modified from another client), before the items leave the feedbag mirror, so
  646. // that deleted buddies' group names can still be found there.
  647. func (s OSCARProxy) Deleted2(me *tocConn, snac wire.SNAC_0x13_0x0A_FeedbagDeleteItem) []string {
  648. var out []string
  649. groupNameByID := me.feedbag.groupNames()
  650. for _, item := range snac.Items {
  651. switch item.ClassID {
  652. case wire.FeedbagClassIdGroup:
  653. out = append(out, fmt.Sprintf("DELETED2:g:%s", item.Name))
  654. case wire.FeedbagClassIdBuddy:
  655. group := groupNameByID[item.GroupID]
  656. if group == "" {
  657. group = "Buddies"
  658. }
  659. out = append(out, fmt.Sprintf("DELETED2:b:%s:%s", item.Name, group))
  660. case wire.FeedbagClassIDDeny:
  661. out = append(out, fmt.Sprintf("DELETED2:d:%s", item.Name))
  662. case wire.FeedbagClassIDPermit:
  663. out = append(out, fmt.Sprintf("DELETED2:p:%s", item.Name))
  664. }
  665. }
  666. return out
  667. }
  668. // ClientEvent handles the CLIENT_EVENT2 TOC2 command.
  669. //
  670. // From BizTOCSock documentation:
  671. //
  672. // I discovered this a while ago, but this is the typing status of a user in IMs, much
  673. // like what AIM does. It is only sent while you're currently being IMed by someone.
  674. // There are only three codes as I know of, but I believe there is one for "User is
  675. // recording..." If it were one, it would probably be code 3.
  676. // 0 = User is doing nothing
  677. // 1 = User has entered text
  678. // 2 = User is currently typing
  679. //
  680. // Command syntax: CLIENT_EVENT2:<Buddy User>:<Typing Status>
  681. func (s OSCARProxy) ClientEvent(snac wire.SNAC_0x04_0x14_ICBMClientEvent) []string {
  682. return []string{fmt.Sprintf("CLIENT_EVENT2:%s:%d", snac.ScreenName, snac.Event)}
  683. }
  684. // userClassString generates the 3-character user class (UC) string based on user flags and away status.
  685. func userClassString(uFlags uint16, isAway bool) string {
  686. uc := [3]string{" ", " ", " "}
  687. if hasFlag(uFlags, wire.OServiceUserFlagAOL) {
  688. uc[0] = "A"
  689. }
  690. if hasFlag(uFlags, wire.OServiceUserFlagAdministrator) {
  691. uc[1] = "A"
  692. } else if hasFlag(uFlags, wire.OServiceUserFlagWireless) {
  693. uc[1] = "C"
  694. } else if hasFlag(uFlags, wire.OServiceUserFlagUnconfirmed) {
  695. uc[1] = "U"
  696. } else if hasFlag(uFlags, wire.OServiceUserFlagOSCARFree) {
  697. uc[1] = "O"
  698. }
  699. if isAway {
  700. uc[2] = "U"
  701. }
  702. return strings.Join(uc[:], "")
  703. }
  704. func sendOrCancel(ctx context.Context, ch chan<- []string, msg []string) {
  705. select {
  706. case <-ctx.Done():
  707. return
  708. case ch <- msg:
  709. return
  710. }
  711. }
  712. // userInfoToUpdateBuddy creates an UPDATE_BUDDY or UPDATE_BUDDY2 server reply from a User
  713. // Info TLV.
  714. func userInfoToUpdateBuddy(snac wire.TLVUserInfo, me *tocConn) string {
  715. online, _ := snac.Uint32BE(wire.OServiceUserInfoSignonTOD)
  716. idle, _ := snac.Uint16BE(wire.OServiceUserInfoIdleTime)
  717. uFlags, _ := snac.Uint16BE(wire.OServiceUserInfoUserFlags)
  718. uc := userClassString(uFlags, snac.IsAway())
  719. warning := fmt.Sprintf("%d", snac.WarningLevel/10)
  720. if me.IsTOC2() {
  721. return fmt.Sprintf("UPDATE_BUDDY2:%s:%s:%s:%d:%d:%s:", snac.ScreenName, "T", warning, online, idle, uc)
  722. }
  723. return fmt.Sprintf("UPDATE_BUDDY:%s:%s:%s:%d:%d:%s", snac.ScreenName, "T", warning, online, idle, uc)
  724. }
  725. // hasFlag checks if a specific flag is set in the bitmask.
  726. func hasFlag[T ~uint16 | ~uint8](bitmask, flag T) bool {
  727. return (bitmask & flag) == flag
  728. }
  729. // userInfoToBuddyCaps creates a BUDDY_CAPS2 server-to-client message from a User Info TLV.
  730. //
  731. // From the BizTOCSock documentation:
  732. //
  733. // These are the buddies capabilities, such as Chat, Live Video, Direct Connect, etc.
  734. // They are sent with every UPDATE_BUDDY2. If a user updates to where they can use
  735. // Direct Connect, you will get sent both packets.
  736. //
  737. // Format: BUDDY_CAPS2:<User>:<Cap1>,<Cap2>,...
  738. func userInfoToBuddyCaps(snac wire.TLVUserInfo, me *tocConn, logger *slog.Logger) string {
  739. if !me.IsTOC2() {
  740. return ""
  741. }
  742. b, hasCaps := snac.Bytes(wire.OServiceUserInfoOscarCaps)
  743. if !hasCaps {
  744. logger.DebugContext(context.Background(), "userInfoToBuddyCaps: no buddy caps found")
  745. return ""
  746. }
  747. if len(b)%16 != 0 {
  748. logger.DebugContext(context.Background(), "userInfoToBuddyCaps: buddy caps length not divisible by 16")
  749. return ""
  750. }
  751. var capStrings []string
  752. for i := 0; i < len(b); i += 16 {
  753. var c [16]byte
  754. copy(c[:], b[i:i+16])
  755. uid := uuid.UUID(c)
  756. capStrings = append(capStrings, uid.String())
  757. }
  758. clientCaps := strings.Join(capStrings, ",")
  759. return fmt.Sprintf("BUDDY_CAPS2:%s:%s", snac.ScreenName, clientCaps)
  760. }