cmd_server.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. package toc
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "net"
  9. "strings"
  10. "time"
  11. "github.com/google/uuid"
  12. "github.com/mk6i/retro-aim-server/state"
  13. "github.com/mk6i/retro-aim-server/wire"
  14. )
  15. var (
  16. cmdInternalSvcErr = "ERROR:989:internal server error"
  17. rateLimitExceededErr = "ERROR:903"
  18. errDisconnect = errors.New("got booted by another session")
  19. )
  20. // RecvBOS routes incoming SNAC messages from the BOS server to their
  21. // corresponding TOC handlers. It ignores any SNAC messages for which there is
  22. // no TOC response.
  23. func (s OSCARProxy) RecvBOS(ctx context.Context, me *state.Session, chatRegistry *ChatRegistry, ch chan<- []byte) error {
  24. for {
  25. select {
  26. case <-ctx.Done():
  27. func() {
  28. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  29. defer cancel()
  30. s.Signout(shutdownCtx, me, chatRegistry)
  31. }()
  32. return nil
  33. case <-me.Closed():
  34. return errDisconnect
  35. case snac := <-me.ReceiveMessage():
  36. switch v := snac.Body.(type) {
  37. case wire.SNAC_0x03_0x0B_BuddyArrived:
  38. sendOrCancel(ctx, ch, s.UpdateBuddyArrival(v))
  39. case wire.SNAC_0x03_0x0C_BuddyDeparted:
  40. sendOrCancel(ctx, ch, s.UpdateBuddyDeparted(v))
  41. case wire.SNAC_0x04_0x07_ICBMChannelMsgToClient:
  42. sendOrCancel(ctx, ch, s.IMIn(ctx, chatRegistry, v))
  43. case wire.SNAC_0x01_0x10_OServiceEvilNotification:
  44. sendOrCancel(ctx, ch, s.Eviled(v))
  45. default:
  46. s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s",
  47. wire.FoodGroupName(snac.Frame.FoodGroup),
  48. wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup)))
  49. }
  50. }
  51. }
  52. }
  53. // RecvChat routes incoming SNAC messages from the chat server to their
  54. // corresponding TOC handlers. It ignores any SNAC messages for which there is
  55. // no TOC response.
  56. func (s OSCARProxy) RecvChat(ctx context.Context, me *state.Session, chatID int, ch chan<- []byte) {
  57. for {
  58. select {
  59. case <-ctx.Done():
  60. return
  61. case <-me.Closed():
  62. return
  63. case snac := <-me.ReceiveMessage():
  64. switch v := snac.Body.(type) {
  65. case wire.SNAC_0x0E_0x04_ChatUsersLeft:
  66. sendOrCancel(ctx, ch, s.ChatUpdateBuddyLeft(v, chatID))
  67. case wire.SNAC_0x0E_0x03_ChatUsersJoined:
  68. sendOrCancel(ctx, ch, s.ChatUpdateBuddyArrived(v, chatID))
  69. case wire.SNAC_0x0E_0x06_ChatChannelMsgToClient:
  70. sendOrCancel(ctx, ch, s.ChatIn(ctx, v, chatID))
  71. default:
  72. s.Logger.DebugContext(ctx, fmt.Sprintf("unsupported snac. foodgroup: %s subgroup: %s",
  73. wire.FoodGroupName(snac.Frame.FoodGroup),
  74. wire.SubGroupName(snac.Frame.FoodGroup, snac.Frame.SubGroup)))
  75. }
  76. }
  77. }
  78. }
  79. // ChatIn handles the CHAT_IN TOC command.
  80. //
  81. // From the TiK documentation:
  82. //
  83. // A chat message was sent in a chat room.
  84. //
  85. // Command syntax: CHAT_IN:<Chat Room Id>:<Source User>:<Whisper? T/F>:<Message>
  86. func (s OSCARProxy) ChatIn(ctx context.Context, snac wire.SNAC_0x0E_0x06_ChatChannelMsgToClient, chatID int) string {
  87. b, ok := snac.Bytes(wire.ChatTLVSenderInformation)
  88. if !ok {
  89. return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVSenderInformation"))
  90. }
  91. u := wire.TLVUserInfo{}
  92. err := wire.UnmarshalBE(&u, bytes.NewReader(b))
  93. if err != nil {
  94. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  95. }
  96. b, ok = snac.Bytes(wire.ChatTLVMessageInfo)
  97. if !ok {
  98. return s.runtimeErr(ctx, errors.New("snac.Bytes: missing wire.ChatTLVMessageInfo"))
  99. }
  100. text, err := wire.UnmarshalChatMessageText(b)
  101. if err != nil {
  102. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalChatMessageText: %w", err))
  103. }
  104. return fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, u.ScreenName, text)
  105. }
  106. // ChatUpdateBuddyArrived handles the CHAT_UPDATE_BUDDY TOC command for chat
  107. // room arrival events.
  108. //
  109. // From the TiK documentation:
  110. //
  111. // This one command handles arrival/departs from a chat room. The very first
  112. // message of this type for each chat room contains the users already in the
  113. // room.
  114. //
  115. // Command syntax: CHAT_UPDATE_BUDDY:<Chat Room Id>:<Inside? T/F>:<User 1>:<User 2>...
  116. func (s OSCARProxy) ChatUpdateBuddyArrived(snac wire.SNAC_0x0E_0x03_ChatUsersJoined, chatID int) string {
  117. users := make([]string, 0, len(snac.Users))
  118. for _, u := range snac.Users {
  119. users = append(users, u.ScreenName)
  120. }
  121. return fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:T:%s", chatID, strings.Join(users, ":"))
  122. }
  123. // ChatUpdateBuddyLeft handles the CHAT_UPDATE_BUDDY TOC command for chat
  124. // room departure events.
  125. //
  126. // From the TiK documentation:
  127. //
  128. // This one command handles arrival/departs from a chat room. The very first
  129. // message of this type for each chat room contains the users already in the
  130. // room.
  131. //
  132. // Command syntax: CHAT_UPDATE_BUDDY:<Chat Room Id>:<Inside? T/F>:<User 1>:<User 2>...
  133. func (s OSCARProxy) ChatUpdateBuddyLeft(snac wire.SNAC_0x0E_0x04_ChatUsersLeft, chatID int) string {
  134. users := make([]string, 0, len(snac.Users))
  135. for _, u := range snac.Users {
  136. users = append(users, u.ScreenName)
  137. }
  138. return fmt.Sprintf("CHAT_UPDATE_BUDDY:%d:F:%s", chatID, strings.Join(users, ":"))
  139. }
  140. // Eviled handles the EVILED TOC command.
  141. //
  142. // From the TiK documentation:
  143. //
  144. // The user was just eviled.
  145. //
  146. // Command syntax: EVILED:<new evil>:<name of eviler, blank if anonymous>
  147. func (s OSCARProxy) Eviled(snac wire.SNAC_0x01_0x10_OServiceEvilNotification) string {
  148. warning := fmt.Sprintf("%d", snac.NewEvil/10)
  149. who := ""
  150. if snac.Snitcher != nil {
  151. who = snac.Snitcher.ScreenName
  152. }
  153. return fmt.Sprintf("EVILED:%s:%s", warning, who)
  154. }
  155. // IMIn handles the IM_IN TOC command.
  156. //
  157. // From the TiK documentation:
  158. //
  159. // Receive an IM from someone. Everything after the third colon is the
  160. // incoming message, including other colons.
  161. //
  162. // Command syntax: IM_IN:<Source User>:<Auto Response T/F?>:<Message>
  163. func (s OSCARProxy) IMIn(ctx context.Context, chatRegistry *ChatRegistry, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
  164. switch snac.ChannelID {
  165. case wire.ICBMChannelIM:
  166. return s.convertICBMInstantMsg(ctx, snac)
  167. case wire.ICBMChannelRendezvous:
  168. return s.convertICBMRendezvous(ctx, chatRegistry, snac)
  169. default:
  170. s.Logger.DebugContext(ctx, "received unsupported ICBM channel message", "channel_id", snac.ChannelID)
  171. return ""
  172. }
  173. }
  174. // convertICBMInstantMsg converts an ICBM instant message SNAC to a TOC IM_IN response.
  175. func (s OSCARProxy) convertICBMInstantMsg(ctx context.Context, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
  176. buf, ok := snac.TLVRestBlock.Bytes(wire.ICBMTLVAOLIMData)
  177. if !ok {
  178. return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing wire.ICBMTLVAOLIMData"))
  179. }
  180. txt, err := wire.UnmarshalICBMMessageText(buf)
  181. if err != nil {
  182. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalICBMMessageText: %w", err))
  183. }
  184. autoResp := "F"
  185. if _, isAutoReply := snac.TLVRestBlock.Bytes(wire.ICBMTLVAutoResponse); isAutoReply {
  186. autoResp = "T"
  187. }
  188. return fmt.Sprintf("IM_IN:%s:%s:%s", snac.ScreenName, autoResp, txt)
  189. }
  190. // convertICBMRendezvous converts an ICBM rendezvous SNAC to a TOC response.
  191. // - if chat, return CHAT_INVITE
  192. // - file transfer, return RVOUS_PROPOSE
  193. // - don't respond for other rendezvous types
  194. func (s OSCARProxy) convertICBMRendezvous(ctx context.Context, chatRegistry *ChatRegistry, snac wire.SNAC_0x04_0x07_ICBMChannelMsgToClient) string {
  195. rdinfo, has := snac.TLVRestBlock.Bytes(wire.ICBMTLVData)
  196. if !has {
  197. return s.runtimeErr(ctx, errors.New("TLVRestBlock.Bytes: missing rendezvous block"))
  198. }
  199. frag := wire.ICBMCh2Fragment{}
  200. if err := wire.UnmarshalBE(&frag, bytes.NewReader(rdinfo)); err != nil {
  201. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  202. }
  203. if frag.Type != wire.ICBMRdvMessagePropose {
  204. s.Logger.DebugContext(ctx, "can't convert ICBM rendezvous message to TOC response", "rdv_type", frag.Type)
  205. return ""
  206. }
  207. switch uuid.UUID(frag.Capability) {
  208. case wire.CapChat:
  209. prompt, ok := frag.Bytes(wire.ICBMRdvTLVTagsInvitation)
  210. if !ok {
  211. return s.runtimeErr(ctx, errors.New("frag.Bytes: missing chat invite prompt"))
  212. }
  213. svcData, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData)
  214. if !ok || svcData == nil {
  215. return s.runtimeErr(ctx, errors.New("frag.Bytes: missing room info"))
  216. }
  217. roomInfo := wire.ICBMRoomInfo{}
  218. if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(svcData)); err != nil {
  219. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  220. }
  221. cookie := strings.Split(roomInfo.Cookie, "-") // make this safe
  222. if len(cookie) < 3 {
  223. return s.runtimeErr(ctx, errors.New("roomInfo.Cookie: malformed cookie, could not get room name"))
  224. }
  225. roomName := cookie[2]
  226. chatID := chatRegistry.Add(roomInfo)
  227. return fmt.Sprintf("CHAT_INVITE:%s:%d:%s:%s", roomName, chatID, snac.ScreenName, prompt)
  228. case wire.CapFileTransfer:
  229. user := snac.TLVUserInfo.ScreenName
  230. capability := strings.ToUpper(wire.CapFileTransfer.String()) // TiK requires upper-case UUID characters
  231. cookie := base64.StdEncoding.EncodeToString(frag.Cookie[:])
  232. seq, _ := frag.Uint16BE(wire.ICBMRdvTLVTagsSeqNum)
  233. rvousIP := "0.0.0.0"
  234. if ip, ok := frag.Bytes(wire.ICBMRdvTLVTagsRdvIP); ok && len(ip) == 4 {
  235. rvousIP = net.IPv4(ip[0], ip[1], ip[2], ip[3]).String()
  236. }
  237. proposerIP := "0.0.0.0"
  238. if ip, ok := frag.Bytes(wire.ICBMRdvTLVTagsRequesterIP); ok && len(ip) == 4 {
  239. proposerIP = net.IPv4(ip[0], ip[1], ip[2], ip[3]).String()
  240. }
  241. verifiedIP := "0.0.0.0"
  242. if ip, ok := frag.Bytes(wire.ICBMRdvTLVTagsVerifiedIP); ok && len(ip) == 4 {
  243. verifiedIP = net.IPv4(ip[0], ip[1], ip[2], ip[3]).String()
  244. }
  245. rvousPort, _ := frag.Uint16BE(wire.ICBMRdvTLVTagsPort)
  246. var fileMetadata string
  247. if f, ok := frag.Bytes(wire.ICBMRdvTLVTagsSvcData); ok {
  248. // remove sequence of null bytes from the end that causes TiK file open
  249. // dialog to crash
  250. f = bytes.TrimRight(f, "\x00")
  251. fileMetadata = base64.StdEncoding.EncodeToString(f)
  252. }
  253. return fmt.Sprintf("RVOUS_PROPOSE:%s:%s:%s:%d:%s:%s:%s:%d:%d:%s",
  254. user, capability, cookie, seq, rvousIP, proposerIP, verifiedIP, rvousPort, wire.ICBMRdvTLVTagsSvcData, fileMetadata)
  255. default:
  256. s.Logger.DebugContext(ctx, "received rendezvous ICBM for unsupported capability", "capability", wire.CapChat)
  257. return ""
  258. }
  259. }
  260. // UpdateBuddyArrival handles the UPDATE_BUDDY TOC command for buddy arrival events.
  261. //
  262. // From the TiK documentation:
  263. //
  264. // 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.
  265. // - uc[0]
  266. // - ' ' - Ignore
  267. // - 'A' - On AOL
  268. // - uc[1]
  269. // - ' ' - Ignore
  270. // - 'A' - Oscar Admin
  271. // - 'U' - Oscar Unconfirmed
  272. // - 'O' - Oscar Normal
  273. // - uc[2]
  274. // - '\0' - Ignore
  275. // - ' ' - Ignore
  276. // - 'U' - The user has set their unavailable flag.
  277. //
  278. // Command syntax: UPDATE_BUDDY:<Buddy User>:<Online? T/F>:<Evil Amount>:<Signon Time>:<IdleTime>:<UC>
  279. func (s OSCARProxy) UpdateBuddyArrival(snac wire.SNAC_0x03_0x0B_BuddyArrived) string {
  280. return userInfoToUpdateBuddy(snac.TLVUserInfo)
  281. }
  282. // UpdateBuddyDeparted handles the UPDATE_BUDDY TOC command for buddy departure events.
  283. //
  284. // From the TiK documentation:
  285. //
  286. // This one command handles arrival/depart/updates. Evil Amount is a
  287. // percentage, Signon Time is UNIX epoc, idle time is in minutes, UC (User
  288. // Class) is a two/three character string.
  289. // - uc[0]
  290. // - ' ' - Ignore
  291. // - 'A' - On AOL
  292. // - uc[1]
  293. // - ' ' - Ignore
  294. // - 'A' - Oscar Admin
  295. // - 'U' - Oscar Unconfirmed
  296. // - 'O' - Oscar Normal
  297. // - uc[2]
  298. // - '\0' - Ignore
  299. // - ' ' - Ignore
  300. // - 'U' - The user has set their unavailable flag.
  301. //
  302. // Command syntax: UPDATE_BUDDY:<Buddy User>:<Online? T/F>:<Evil Amount>:<Signon Time>:<IdleTime>:<UC>
  303. func (s OSCARProxy) UpdateBuddyDeparted(snac wire.SNAC_0x03_0x0C_BuddyDeparted) string {
  304. return fmt.Sprintf("UPDATE_BUDDY:%s:F:0:0:0: ", snac.ScreenName)
  305. }
  306. func sendOrCancel(ctx context.Context, ch chan<- []byte, msg string) {
  307. select {
  308. case <-ctx.Done():
  309. return
  310. case ch <- []byte(msg):
  311. return
  312. }
  313. }
  314. // userInfoToUpdateBuddy creates an UPDATE_BUDDY server reply from a User
  315. // Info TLV.
  316. func userInfoToUpdateBuddy(snac wire.TLVUserInfo) string {
  317. online, _ := snac.Uint32BE(wire.OServiceUserInfoSignonTOD)
  318. idle, _ := snac.Uint16BE(wire.OServiceUserInfoIdleTime)
  319. uc := [3]string{" ", "O", " "}
  320. if snac.IsAway() {
  321. uc[2] = "U"
  322. }
  323. warning := fmt.Sprintf("%d", snac.WarningLevel/10)
  324. class := strings.Join(uc[:], "")
  325. return fmt.Sprintf("UPDATE_BUDDY:%s:%s:%s:%d:%d:%s", snac.ScreenName, "T", warning, online, idle, class)
  326. }