cmd_client.go 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828
  1. package toc
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "encoding/csv"
  7. "encoding/hex"
  8. "errors"
  9. "fmt"
  10. "log/slog"
  11. "net/url"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/google/uuid"
  17. "github.com/mk6i/retro-aim-server/config"
  18. "github.com/mk6i/retro-aim-server/state"
  19. "github.com/mk6i/retro-aim-server/wire"
  20. )
  21. // NewChatRegistry creates a new ChatRegistry instances.
  22. func NewChatRegistry() *ChatRegistry {
  23. chatRegistry := &ChatRegistry{
  24. lookup: make(map[int]wire.ICBMRoomInfo),
  25. sessions: make(map[int]*state.Session),
  26. m: sync.RWMutex{},
  27. }
  28. return chatRegistry
  29. }
  30. // ChatRegistry manages the chat rooms that a user is connected to during a TOC
  31. // session. It maintains mappings between chat room identifiers, metadata, and
  32. // active chat sessions.
  33. //
  34. // This struct provides thread-safe operations for adding, retrieving, and managing
  35. // chat room metadata and associated sessions.
  36. type ChatRegistry struct {
  37. lookup map[int]wire.ICBMRoomInfo // Maps chat room IDs to their metadata.
  38. sessions map[int]*state.Session // Tracks active chat sessions by chat room ID.
  39. nextID int // Incremental identifier for newly added chat rooms.
  40. m sync.RWMutex // Synchronization primitive for concurrent access.
  41. }
  42. // Add registers metadata for a newly joined chat room and returns a unique
  43. // identifier for it. If the room is already registered, it returns the existing ID.
  44. func (c *ChatRegistry) Add(room wire.ICBMRoomInfo) int {
  45. c.m.Lock()
  46. defer c.m.Unlock()
  47. for chatID, r := range c.lookup {
  48. if r == room {
  49. return chatID
  50. }
  51. }
  52. id := c.nextID
  53. c.lookup[id] = room
  54. c.nextID++
  55. return id
  56. }
  57. // LookupRoom retrieves metadata for the chat room registered with chatID.
  58. // It returns the room metadata and a boolean indicating whether the chat ID
  59. // was found.
  60. func (c *ChatRegistry) LookupRoom(chatID int) (wire.ICBMRoomInfo, bool) {
  61. c.m.RLock()
  62. defer c.m.RUnlock()
  63. room, found := c.lookup[chatID]
  64. return room, found
  65. }
  66. // RegisterSess associates a chat session with a chat room. If a session is
  67. // already registered for the given chat ID, it will be overwritten.
  68. func (c *ChatRegistry) RegisterSess(chatID int, sess *state.Session) {
  69. c.m.Lock()
  70. defer c.m.Unlock()
  71. c.sessions[chatID] = sess
  72. }
  73. // RetrieveSess retrieves the chat session associated with the given chat ID.
  74. // If no session is registered for the chat ID, it returns nil.
  75. func (c *ChatRegistry) RetrieveSess(chatID int) *state.Session {
  76. c.m.RLock()
  77. defer c.m.RUnlock()
  78. return c.sessions[chatID]
  79. }
  80. // RemoveSess removes a chat session.
  81. func (c *ChatRegistry) RemoveSess(chatID int) {
  82. c.m.Lock()
  83. defer c.m.Unlock()
  84. delete(c.sessions, chatID)
  85. }
  86. // Sessions retrieves all the chat sessions.
  87. func (c *ChatRegistry) Sessions() []*state.Session {
  88. c.m.RLock()
  89. defer c.m.RUnlock()
  90. sessions := make([]*state.Session, 0, len(c.sessions))
  91. for _, s := range c.sessions {
  92. sessions = append(sessions, s)
  93. }
  94. return sessions
  95. }
  96. // OSCARProxy acts as a bridge between TOC clients and the OSCAR server,
  97. // translating protocol messages between the two.
  98. //
  99. // It performs the following functions:
  100. // - Receives TOC messages from the client, converts them into SNAC messages,
  101. // and forwards them to the OSCAR server. The SNAC response is then converted
  102. // back into a TOC response for the client.
  103. // - Receives incoming messages from the OSCAR server and translates them into
  104. // TOC responses for the client.
  105. type OSCARProxy struct {
  106. AdminService AdminService
  107. AuthService AuthService
  108. BuddyListRegistry BuddyListRegistry
  109. BuddyService BuddyService
  110. ChatNavService ChatNavService
  111. ChatService ChatService
  112. CookieBaker CookieBaker
  113. DirSearchService DirSearchService
  114. ICBMService ICBMService
  115. LocateService LocateService
  116. Logger *slog.Logger
  117. OServiceService OServiceService
  118. PermitDenyService PermitDenyService
  119. TOCConfigStore TOCConfigStore
  120. SNACRateLimits wire.SNACRateLimits
  121. HTTPIPRateLimiter *IPRateLimiter
  122. }
  123. // RecvClientCmd processes a client TOC command and returns a server reply.
  124. //
  125. // * sessBOS is the current user's session.
  126. // * chatRegistry manages the current user's chat sessions
  127. // * payload is the command + arguments
  128. // * toCh is the channel that transports messages to client
  129. // * doAsync performs async tasks, is auto-cleaned up by caller
  130. //
  131. // It returns true if the server can continue processing commands.
  132. func (s OSCARProxy) RecvClientCmd(
  133. ctx context.Context,
  134. sessBOS *state.Session,
  135. chatRegistry *ChatRegistry,
  136. payload []byte,
  137. toCh chan<- []byte,
  138. doAsync func(f func() error),
  139. ) (reply string) {
  140. cmd := payload
  141. var args []byte
  142. if idx := bytes.IndexByte(payload, ' '); idx > -1 {
  143. cmd, args = payload[:idx], payload[idx:]
  144. }
  145. if s.Logger.Enabled(ctx, slog.LevelDebug) {
  146. s.Logger.DebugContext(ctx, "client request", "command", payload)
  147. } else {
  148. s.Logger.InfoContext(ctx, "client request", "command", cmd)
  149. }
  150. switch string(cmd) {
  151. case "toc_send_im":
  152. return s.SendIM(ctx, sessBOS, args)
  153. case "toc_init_done":
  154. return s.InitDone(ctx, sessBOS)
  155. case "toc_add_buddy":
  156. return s.AddBuddy(ctx, sessBOS, args)
  157. case "toc_get_status":
  158. return s.GetStatus(ctx, sessBOS, args)
  159. case "toc_remove_buddy":
  160. return s.RemoveBuddy(ctx, sessBOS, args)
  161. case "toc_add_permit":
  162. return s.AddPermit(ctx, sessBOS, args)
  163. case "toc_add_deny":
  164. return s.AddDeny(ctx, sessBOS, args)
  165. case "toc_set_away":
  166. return s.SetAway(ctx, sessBOS, args)
  167. case "toc_set_caps":
  168. return s.SetCaps(ctx, sessBOS, args)
  169. case "toc_evil":
  170. return s.Evil(ctx, sessBOS, args)
  171. case "toc_get_info":
  172. return s.GetInfoURL(ctx, sessBOS, args)
  173. case "toc_change_passwd":
  174. return s.ChangePassword(ctx, sessBOS, args)
  175. case "toc_format_nickname":
  176. return s.FormatNickname(ctx, sessBOS, args)
  177. case "toc_chat_join", "toc_chat_accept":
  178. var chatID int
  179. var msg string
  180. if string(cmd) == "toc_chat_join" {
  181. chatID, msg = s.ChatJoin(ctx, sessBOS, chatRegistry, args)
  182. } else {
  183. chatID, msg = s.ChatAccept(ctx, sessBOS, chatRegistry, args)
  184. }
  185. if msg == cmdInternalSvcErr {
  186. return msg
  187. }
  188. doAsync(func() error {
  189. sess := chatRegistry.RetrieveSess(chatID)
  190. s.RecvChat(ctx, sess, chatID, toCh)
  191. return nil
  192. })
  193. return msg
  194. case "toc_chat_send":
  195. return s.ChatSend(ctx, chatRegistry, args)
  196. case "toc_chat_whisper":
  197. return s.ChatWhisper(ctx, chatRegistry, args)
  198. case "toc_chat_leave":
  199. return s.ChatLeave(ctx, chatRegistry, args)
  200. case "toc_set_info":
  201. return s.SetInfo(ctx, sessBOS, args)
  202. case "toc_set_dir":
  203. return s.SetDir(ctx, sessBOS, args)
  204. case "toc_set_idle":
  205. return s.SetIdle(ctx, sessBOS, args)
  206. case "toc_set_config":
  207. return s.SetConfig(ctx, sessBOS, args)
  208. case "toc_chat_invite":
  209. return s.ChatInvite(ctx, sessBOS, chatRegistry, args)
  210. case "toc_dir_search":
  211. return s.GetDirSearchURL(ctx, sessBOS, args)
  212. case "toc_get_dir":
  213. return s.GetDirURL(ctx, sessBOS, args)
  214. case "toc_rvous_accept":
  215. return s.RvousAccept(ctx, sessBOS, args)
  216. case "toc_rvous_cancel":
  217. return s.RvousCancel(ctx, sessBOS, args)
  218. }
  219. s.Logger.ErrorContext(ctx, fmt.Sprintf("unsupported TOC command %s", cmd))
  220. return cmdInternalSvcErr
  221. }
  222. // AddBuddy handles the toc_add_buddy TOC command.
  223. //
  224. // From the TiK documentation:
  225. //
  226. // Add buddies to your buddy list. This does not change your saved config.
  227. //
  228. // Command syntax: toc_add_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
  229. func (s OSCARProxy) AddBuddy(ctx context.Context, me *state.Session, args []byte) string {
  230. if msg, isLimited := s.checkRateLimit(ctx, me, wire.Buddy, wire.BuddyAddBuddies); isLimited {
  231. return msg
  232. }
  233. users, err := parseArgs(args)
  234. if err != nil {
  235. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  236. }
  237. snac := wire.SNAC_0x03_0x04_BuddyAddBuddies{}
  238. for _, sn := range users {
  239. snac.Buddies = append(snac.Buddies, struct {
  240. ScreenName string `oscar:"len_prefix=uint8"`
  241. }{ScreenName: sn})
  242. }
  243. if err := s.BuddyService.AddBuddies(ctx, me, snac); err != nil {
  244. return s.runtimeErr(ctx, fmt.Errorf("BuddyService.AddBuddies: %w", err))
  245. }
  246. return ""
  247. }
  248. // AddPermit handles the toc_add_permit TOC command.
  249. //
  250. // From the TiK documentation:
  251. //
  252. // ADD the following people to your permit mode. If you are in deny mode it
  253. // will switch you to permit mode first. With no arguments and in deny mode
  254. // this will switch you to permit none. If already in permit mode, no
  255. // arguments does nothing and your permit list remains the same.
  256. //
  257. // Command syntax: toc_add_permit [ <User 1> [<User 2> [...]]]
  258. func (s OSCARProxy) AddPermit(ctx context.Context, me *state.Session, args []byte) string {
  259. if msg, isLimited := s.checkRateLimit(ctx, me, wire.PermitDeny, wire.PermitDenyAddDenyListEntries); isLimited {
  260. return msg
  261. }
  262. users, err := parseArgs(args)
  263. if err != nil {
  264. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  265. }
  266. snac := wire.SNAC_0x09_0x05_PermitDenyAddPermListEntries{}
  267. for _, sn := range users {
  268. snac.Users = append(snac.Users, struct {
  269. ScreenName string `oscar:"len_prefix=uint8"`
  270. }{ScreenName: sn})
  271. }
  272. if err := s.PermitDenyService.AddPermListEntries(ctx, me, snac); err != nil {
  273. return s.runtimeErr(ctx, fmt.Errorf("PermitDenyService.AddPermListEntries: %w", err))
  274. }
  275. return ""
  276. }
  277. // AddDeny handles the toc_add_deny TOC command.
  278. //
  279. // From the TiK documentation:
  280. //
  281. // ADD the following people to your deny mode. If you are in permit mode it
  282. // will switch you to deny mode first. With no arguments and in permit mode,
  283. // this will switch you to deny none. If already in deny mode, no arguments
  284. // does nothing and your deny list remains unchanged.
  285. //
  286. // Command syntax: toc_add_deny [ <User 1> [<User 2> [...]]]
  287. func (s OSCARProxy) AddDeny(ctx context.Context, me *state.Session, args []byte) string {
  288. if msg, isLimited := s.checkRateLimit(ctx, me, wire.PermitDeny, wire.PermitDenyAddDenyListEntries); isLimited {
  289. return msg
  290. }
  291. users, err := parseArgs(args)
  292. if err != nil {
  293. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  294. }
  295. snac := wire.SNAC_0x09_0x07_PermitDenyAddDenyListEntries{}
  296. for _, sn := range users {
  297. snac.Users = append(snac.Users, struct {
  298. ScreenName string `oscar:"len_prefix=uint8"`
  299. }{ScreenName: sn})
  300. }
  301. if err := s.PermitDenyService.AddDenyListEntries(ctx, me, snac); err != nil {
  302. return s.runtimeErr(ctx, fmt.Errorf("PermitDenyService.AddDenyListEntries: %w", err))
  303. }
  304. return ""
  305. }
  306. // ChangePassword handles the toc_change_passwd TOC command.
  307. //
  308. // From the TiK documentation:
  309. //
  310. // Change a user's password. An ADMIN_PASSWD_STATUS or ERROR message will be
  311. // sent back to the client.
  312. //
  313. // Command syntax: toc_change_passwd <existing_passwd> <new_passwd>
  314. func (s OSCARProxy) ChangePassword(ctx context.Context, me *state.Session, args []byte) string {
  315. if msg, isLimited := s.checkRateLimit(ctx, me, wire.Admin, wire.AdminInfoChangeRequest); isLimited {
  316. return msg
  317. }
  318. var oldPass, newPass string
  319. if _, err := parseArgs(args, &oldPass, &newPass); err != nil {
  320. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  321. }
  322. oldPass = unescape(oldPass)
  323. newPass = unescape(newPass)
  324. reqSNAC := wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  325. TLVRestBlock: wire.TLVRestBlock{
  326. TLVList: wire.TLVList{
  327. wire.NewTLVBE(wire.AdminTLVOldPassword, oldPass),
  328. wire.NewTLVBE(wire.AdminTLVNewPassword, newPass),
  329. },
  330. },
  331. }
  332. reply, err := s.AdminService.InfoChangeRequest(ctx, me, wire.SNACFrame{}, reqSNAC)
  333. if err != nil {
  334. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: %w", err))
  335. }
  336. replyBody, ok := reply.Body.(wire.SNAC_0x07_0x05_AdminChangeReply)
  337. if !ok {
  338. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", replyBody))
  339. }
  340. code, ok := replyBody.Uint16BE(wire.AdminTLVErrorCode)
  341. if ok {
  342. switch code {
  343. case wire.AdminInfoErrorInvalidPasswordLength:
  344. return "ERROR:911"
  345. case wire.AdminInfoErrorValidatePassword:
  346. return "ERROR:912"
  347. default:
  348. return "ERROR:913"
  349. }
  350. }
  351. return "ADMIN_PASSWD_STATUS:0"
  352. }
  353. // ChatAccept handles the toc_chat_accept TOC command.
  354. //
  355. // From the TiK documentation:
  356. //
  357. // Accept a CHAT_INVITE message from TOC. The server will send a CHAT_JOIN in
  358. // response.
  359. //
  360. // Command syntax: toc_chat_accept <Chat Room ID>
  361. func (s OSCARProxy) ChatAccept(
  362. ctx context.Context,
  363. me *state.Session,
  364. chatRegistry *ChatRegistry,
  365. args []byte,
  366. ) (int, string) {
  367. var chatIDStr string
  368. if _, err := parseArgs(args, &chatIDStr); err != nil {
  369. return 0, s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  370. }
  371. chatID, err := strconv.Atoi(chatIDStr)
  372. if err != nil {
  373. return 0, s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  374. }
  375. chatInfo, found := chatRegistry.LookupRoom(chatID)
  376. if !found {
  377. return 0, s.runtimeErr(ctx, fmt.Errorf("chatRegistry.LookupRoom: no chat found for ID %d", chatID))
  378. }
  379. if msg, isLimited := s.checkRateLimit(ctx, me, wire.ChatNav, wire.ChatNavRequestRoomInfo); isLimited {
  380. return 0, msg
  381. }
  382. reqRoomSNAC := wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
  383. Cookie: chatInfo.Cookie,
  384. Exchange: chatInfo.Exchange,
  385. InstanceNumber: chatInfo.Instance,
  386. }
  387. reqRoomReply, err := s.ChatNavService.RequestRoomInfo(ctx, wire.SNACFrame{}, reqRoomSNAC)
  388. if err != nil {
  389. return 0, s.runtimeErr(ctx, fmt.Errorf("ChatNavService.RequestRoomInfo: %w", err))
  390. }
  391. reqRoomReplyBody, ok := reqRoomReply.Body.(wire.SNAC_0x0D_0x09_ChatNavNavInfo)
  392. if !ok {
  393. return 0, s.runtimeErr(
  394. ctx,
  395. fmt.Errorf("chatNavService.RequestRoomInfo: unexpected response type %v", reqRoomReplyBody),
  396. )
  397. }
  398. b, hasInfo := reqRoomReplyBody.Bytes(wire.ChatNavTLVRoomInfo)
  399. if !hasInfo {
  400. return 0, s.runtimeErr(ctx, errors.New("reqRoomReplyBody.Bytes: missing wire.ChatNavTLVRoomInfo"))
  401. }
  402. roomInfo := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
  403. if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(b)); err != nil {
  404. return 0, s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  405. }
  406. roomName, hasName := roomInfo.Bytes(wire.ChatRoomTLVRoomName)
  407. if !hasName {
  408. return 0, s.runtimeErr(ctx, errors.New("roomInfo.Bytes: missing wire.ChatRoomTLVRoomName"))
  409. }
  410. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceServiceRequest); isLimited {
  411. return 0, msg
  412. }
  413. svcReqSNAC := wire.SNAC_0x01_0x04_OServiceServiceRequest{
  414. FoodGroup: wire.Chat,
  415. TLVRestBlock: wire.TLVRestBlock{
  416. TLVList: wire.TLVList{
  417. wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
  418. Cookie: chatInfo.Cookie,
  419. }),
  420. },
  421. },
  422. }
  423. svcReqReply, err := s.OServiceService.ServiceRequest(ctx, wire.BOS, me, wire.SNACFrame{}, svcReqSNAC, config.Listener{})
  424. if err != nil {
  425. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.ServiceRequest: %w", err))
  426. }
  427. svcReqReplyBody, ok := svcReqReply.Body.(wire.SNAC_0x01_0x05_OServiceServiceResponse)
  428. if !ok {
  429. return 0, s.runtimeErr(
  430. ctx,
  431. fmt.Errorf("OServiceServiceBOS.ServiceRequest: unexpected response type %v", svcReqReplyBody),
  432. )
  433. }
  434. loginCookie, hasCookie := svcReqReplyBody.Bytes(wire.OServiceTLVTagsLoginCookie)
  435. if !hasCookie {
  436. return 0, s.runtimeErr(ctx, errors.New("missing wire.OServiceTLVTagsLoginCookie"))
  437. }
  438. // todo: naming for cookie: login cookie, server cookie, or auth cookie?
  439. serverCookie, err := s.AuthService.CrackCookie(loginCookie)
  440. chatSess, err := s.AuthService.RegisterChatSession(ctx, serverCookie)
  441. if err != nil {
  442. return 0, s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterChatSession: %w", err))
  443. }
  444. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceClientOnline); isLimited {
  445. return 0, msg
  446. }
  447. if err := s.OServiceService.ClientOnline(ctx, wire.Chat, wire.SNAC_0x01_0x02_OServiceClientOnline{}, chatSess); err != nil {
  448. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceChat.ClientOnline: %w", err))
  449. }
  450. chatRegistry.RegisterSess(chatID, chatSess)
  451. return chatID, fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)
  452. }
  453. // ChatInvite handles the toc_chat_invite TOC command.
  454. //
  455. // From the TiK documentation:
  456. //
  457. // Once you are inside a chat room you can invite other people into that room.
  458. // Remember to quote and encode the invite message.
  459. //
  460. // Command syntax: toc_chat_invite <Chat Room ID> <Invite Msg> <buddy1> [<buddy2> [<buddy3> [...]]]
  461. func (s OSCARProxy) ChatInvite(ctx context.Context, me *state.Session, chatRegistry *ChatRegistry, args []byte) string {
  462. var chatRoomIDStr, msg string
  463. users, err := parseArgs(args, &chatRoomIDStr, &msg)
  464. if err != nil {
  465. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  466. }
  467. msg = unescape(msg)
  468. chatID, err := strconv.Atoi(chatRoomIDStr)
  469. if err != nil {
  470. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  471. }
  472. roomInfo, found := chatRegistry.LookupRoom(chatID)
  473. if !found {
  474. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.LookupRoom: chat ID `%d` not found", chatID))
  475. }
  476. for _, guest := range users {
  477. if msg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  478. return msg
  479. }
  480. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  481. ChannelID: wire.ICBMChannelRendezvous,
  482. ScreenName: guest,
  483. TLVRestBlock: wire.TLVRestBlock{
  484. TLVList: wire.TLVList{
  485. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  486. Type: wire.ICBMRdvMessagePropose,
  487. Capability: wire.CapChat,
  488. TLVRestBlock: wire.TLVRestBlock{
  489. TLVList: wire.TLVList{
  490. wire.NewTLVBE(wire.ICBMRdvTLVTagsSeqNum, uint16(1)),
  491. wire.NewTLVBE(wire.ICBMRdvTLVTagsInvitation, msg),
  492. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMECharset, "us-ascii"),
  493. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMELang, "en"),
  494. wire.NewTLVBE(wire.ICBMRdvTLVTagsSvcData, roomInfo),
  495. },
  496. },
  497. }),
  498. },
  499. },
  500. }
  501. if _, err := s.ICBMService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  502. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  503. }
  504. }
  505. return ""
  506. }
  507. // ChatJoin handles the toc_chat_join TOC command.
  508. //
  509. // From the TiK documentation:
  510. //
  511. // Join a chat room in the given exchange. Exchange is an integer that
  512. // represents a group of chat rooms. Different exchanges have different
  513. // properties. For example some exchanges might have room replication (ie a
  514. // room never fills up, there are just multiple instances.) and some exchanges
  515. // might have navigational information. Currently, exchange should always be
  516. // 4, however this may change in the future. You will either receive an ERROR
  517. // if the room couldn't be joined or a CHAT_JOIN message. The Chat Room Name
  518. // is case-insensitive and consecutive spaces are removed.
  519. //
  520. // Command syntax: toc_chat_join <Exchange> <Chat Room Name>
  521. func (s OSCARProxy) ChatJoin(
  522. ctx context.Context,
  523. me *state.Session,
  524. chatRegistry *ChatRegistry,
  525. args []byte,
  526. ) (int, string) {
  527. var exchangeStr, roomName string
  528. if _, err := parseArgs(args, &exchangeStr, &roomName); err != nil {
  529. return 0, s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  530. }
  531. roomName = unescape(roomName)
  532. // create room or retrieve the room if it already exists
  533. exchange, err := strconv.Atoi(exchangeStr)
  534. if err != nil {
  535. return 0, s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  536. }
  537. if msg, isLimited := s.checkRateLimit(ctx, me, wire.Chat, wire.ChatRoomInfoUpdate); isLimited {
  538. return 0, msg
  539. }
  540. mkRoomReq := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  541. Exchange: uint16(exchange),
  542. Cookie: "create",
  543. TLVBlock: wire.TLVBlock{
  544. TLVList: wire.TLVList{
  545. wire.NewTLVBE(wire.ChatRoomTLVRoomName, roomName),
  546. },
  547. },
  548. }
  549. mkRoomReply, err := s.ChatNavService.CreateRoom(ctx, me, wire.SNACFrame{}, mkRoomReq)
  550. if err != nil {
  551. return 0, s.runtimeErr(ctx, fmt.Errorf("ChatNavService.CreateRoom: %w", err))
  552. }
  553. mkRoomReplyBody, ok := mkRoomReply.Body.(wire.SNAC_0x0D_0x09_ChatNavNavInfo)
  554. if !ok {
  555. return 0, s.runtimeErr(
  556. ctx,
  557. fmt.Errorf("chatNavService.CreateRoom: unexpected response type %v", mkRoomReplyBody),
  558. )
  559. }
  560. buf, ok := mkRoomReplyBody.Bytes(wire.ChatNavTLVRoomInfo)
  561. if !ok {
  562. return 0, s.runtimeErr(ctx, errors.New("mkRoomReplyBody.Bytes: missing wire.ChatNavTLVRoomInfo"))
  563. }
  564. inBody := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
  565. if err := wire.UnmarshalBE(&inBody, bytes.NewReader(buf)); err != nil {
  566. return 0, s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  567. }
  568. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceServiceRequest); isLimited {
  569. return 0, msg
  570. }
  571. svcReqSNAC := wire.SNAC_0x01_0x04_OServiceServiceRequest{
  572. FoodGroup: wire.Chat,
  573. TLVRestBlock: wire.TLVRestBlock{
  574. TLVList: wire.TLVList{
  575. wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
  576. Cookie: inBody.Cookie,
  577. }),
  578. },
  579. },
  580. }
  581. svcReqReply, err := s.OServiceService.ServiceRequest(ctx, wire.BOS, me, wire.SNACFrame{}, svcReqSNAC, config.Listener{})
  582. if err != nil {
  583. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.ServiceRequest: %w", err))
  584. }
  585. svcReqReplyBody, ok := svcReqReply.Body.(wire.SNAC_0x01_0x05_OServiceServiceResponse)
  586. if !ok {
  587. return 0, s.runtimeErr(
  588. ctx,
  589. fmt.Errorf("OServiceServiceBOS.ServiceRequest: unexpected response type %v", svcReqReplyBody),
  590. )
  591. }
  592. loginCookie, hasCookie := svcReqReplyBody.Bytes(wire.OServiceTLVTagsLoginCookie)
  593. if !hasCookie {
  594. return 0, s.runtimeErr(ctx, errors.New("svcReqReplyBody.Bytes: missing wire.OServiceTLVTagsLoginCookie"))
  595. }
  596. // todo: naming for cookie: login cookie, server cookie, or auth cookie?
  597. serverCookie, err := s.AuthService.CrackCookie(loginCookie)
  598. chatSess, err := s.AuthService.RegisterChatSession(ctx, serverCookie)
  599. if err != nil {
  600. return 0, s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterChatSession: %w", err))
  601. }
  602. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceClientOnline); isLimited {
  603. return 0, msg
  604. }
  605. if err := s.OServiceService.ClientOnline(ctx, wire.Chat, wire.SNAC_0x01_0x02_OServiceClientOnline{}, chatSess); err != nil {
  606. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceChat.ClientOnline: %w", err))
  607. }
  608. roomInfo := wire.ICBMRoomInfo{
  609. Exchange: inBody.Exchange,
  610. Cookie: inBody.Cookie,
  611. Instance: inBody.InstanceNumber,
  612. }
  613. chatID := chatRegistry.Add(roomInfo)
  614. chatRegistry.RegisterSess(chatID, chatSess)
  615. return chatID, fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)
  616. }
  617. // ChatLeave handles the toc_chat_leave TOC command.
  618. //
  619. // From the TiK documentation:
  620. //
  621. // Leave the chat room.
  622. //
  623. // Command syntax: toc_chat_leave <Chat Room ID>
  624. func (s OSCARProxy) ChatLeave(ctx context.Context, chatRegistry *ChatRegistry, args []byte) string {
  625. var chatIDStr string
  626. if _, err := parseArgs(args, &chatIDStr); err != nil {
  627. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  628. }
  629. chatID, err := strconv.Atoi(chatIDStr)
  630. if err != nil {
  631. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  632. }
  633. me := chatRegistry.RetrieveSess(chatID)
  634. if me == nil {
  635. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveSess: chat session `%d` not found", chatID))
  636. }
  637. s.AuthService.SignoutChat(ctx, me)
  638. me.Close() // stop async server SNAC reply handler for this chat room
  639. chatRegistry.RemoveSess(chatID)
  640. return fmt.Sprintf("CHAT_LEFT:%d", chatID)
  641. }
  642. // ChatSend handles the toc_chat_send TOC command.
  643. //
  644. // From the TiK documentation:
  645. //
  646. // Send a message in a chat room using the chat room id from CHAT_JOIN. Since
  647. // reflection is always on in TOC, you do not need to add the message to your
  648. // chat UI, since you will get a CHAT_IN with the message. Remember to quote
  649. // and encode the message.
  650. //
  651. // Command syntax: toc_chat_send <Chat Room ID> <Message>
  652. func (s OSCARProxy) ChatSend(ctx context.Context, chatRegistry *ChatRegistry, args []byte) string {
  653. var chatIDStr, msg string
  654. if _, err := parseArgs(args, &chatIDStr, &msg); err != nil {
  655. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  656. }
  657. msg = unescape(msg)
  658. chatID, err := strconv.Atoi(chatIDStr)
  659. if err != nil {
  660. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  661. }
  662. me := chatRegistry.RetrieveSess(chatID)
  663. if me == nil {
  664. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveSess: session for chat ID `%d` not found", chatID))
  665. }
  666. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Chat, wire.ChatChannelMsgToHost); isLimited {
  667. return errMsg
  668. }
  669. block := wire.TLVRestBlock{}
  670. // the order of these TLVs matters for AIM 2.x. if out of order, screen
  671. // names do not appear with each chat message.
  672. block.Append(wire.NewTLVBE(wire.ChatTLVEnableReflectionFlag, uint8(1)))
  673. block.Append(wire.NewTLVBE(wire.ChatTLVSenderInformation, me.TLVUserInfo()))
  674. block.Append(wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}))
  675. block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  676. TLVList: wire.TLVList{
  677. wire.NewTLVBE(wire.ChatTLVMessageInfoText, msg),
  678. },
  679. }))
  680. snac := wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  681. Channel: wire.ICBMChannelMIME,
  682. TLVRestBlock: block,
  683. }
  684. reply, err := s.ChatService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac)
  685. if err != nil {
  686. return s.runtimeErr(ctx, fmt.Errorf("ChatService.ChannelMsgToHost: %w", err))
  687. }
  688. if reply == nil {
  689. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: missing response "))
  690. }
  691. switch v := reply.Body.(type) {
  692. case wire.SNAC_0x0E_0x06_ChatChannelMsgToClient:
  693. msgInfo, ok := v.Bytes(wire.ChatTLVMessageInfo)
  694. if !ok {
  695. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: missing wire.ChatTLVMessageInfo"))
  696. }
  697. reflectMsg, err := wire.UnmarshalChatMessageText(msgInfo)
  698. if err != nil {
  699. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalChatMessageText: %w", err))
  700. }
  701. senderInfo, ok := v.Bytes(wire.ChatTLVSenderInformation)
  702. if !ok {
  703. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: missing wire.ChatTLVSenderInformation"))
  704. }
  705. var userInfo wire.TLVUserInfo
  706. if err := wire.UnmarshalBE(&userInfo, bytes.NewReader(senderInfo)); err != nil {
  707. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  708. }
  709. return fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, userInfo.ScreenName, reflectMsg)
  710. default:
  711. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: unexpected response"))
  712. }
  713. }
  714. // ChatWhisper handles the toc_chat_send TOC command.
  715. //
  716. // From the TiK documentation:
  717. //
  718. // Send a message in a chat room using the chat room id from CHAT_JOIN.
  719. // This message is directed at only one person. (Currently you DO need to add
  720. // this to your UI.) Remember to quote and encode the message. Chat whispering
  721. // is different from IMs since it is linked to a chat room, and should usually
  722. // be displayed in the chat room UI.
  723. //
  724. // Command syntax: toc_chat_whisper <Chat Room ID> <dst_user> <Message>
  725. func (s OSCARProxy) ChatWhisper(ctx context.Context, chatRegistry *ChatRegistry, args []byte) string {
  726. var chatIDStr, recip, msg string
  727. if _, err := parseArgs(args, &chatIDStr, &recip, &msg); err != nil {
  728. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  729. }
  730. msg = unescape(msg)
  731. chatID, err := strconv.Atoi(chatIDStr)
  732. if err != nil {
  733. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  734. }
  735. me := chatRegistry.RetrieveSess(chatID)
  736. if me == nil {
  737. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveSess: session for chat ID `%d` not found", chatID))
  738. }
  739. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Chat, wire.ChatChannelMsgToHost); isLimited {
  740. return errMsg
  741. }
  742. block := wire.TLVRestBlock{}
  743. block.Append(wire.NewTLVBE(wire.ChatTLVSenderInformation, me.TLVUserInfo()))
  744. block.Append(wire.NewTLVBE(wire.ChatTLVWhisperToUser, recip))
  745. block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  746. TLVList: wire.TLVList{
  747. wire.NewTLVBE(wire.ChatTLVMessageInfoText, msg),
  748. },
  749. }))
  750. snac := wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  751. Channel: wire.ICBMChannelMIME,
  752. TLVRestBlock: block,
  753. }
  754. if _, err = s.ChatService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  755. return s.runtimeErr(ctx, fmt.Errorf("ChatService.ChannelMsgToHost: %w", err))
  756. }
  757. return ""
  758. }
  759. // Evil handles the toc_evil TOC command.
  760. //
  761. // From the TiK documentation:
  762. //
  763. // Evil/Warn someone else. The 2nd argument is either the string "norm" for a
  764. // normal warning, or "anon" for an anonymous warning. You can only evil
  765. // people who have recently sent you ims. The higher someones evil level, the
  766. // slower they can send message.
  767. //
  768. // Command syntax: toc_evil <User> <norm|anon>
  769. func (s OSCARProxy) Evil(ctx context.Context, me *state.Session, args []byte) string {
  770. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMEvilRequest); isLimited {
  771. return errMsg
  772. }
  773. var user, scope string
  774. if _, err := parseArgs(args, &user, &scope); err != nil {
  775. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  776. }
  777. snac := wire.SNAC_0x04_0x08_ICBMEvilRequest{
  778. ScreenName: user,
  779. }
  780. switch scope {
  781. case "anon":
  782. snac.SendAs = 1
  783. case "norm":
  784. snac.SendAs = 0
  785. default:
  786. return s.runtimeErr(ctx, fmt.Errorf("incorrect warning type `%s`. allowed values: anon, norm", scope))
  787. }
  788. response, err := s.ICBMService.EvilRequest(ctx, me, wire.SNACFrame{}, snac)
  789. if err != nil {
  790. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.EvilRequest: %w", err))
  791. }
  792. switch v := response.Body.(type) {
  793. case wire.SNAC_0x04_0x09_ICBMEvilReply:
  794. return ""
  795. case wire.SNACError:
  796. s.Logger.InfoContext(ctx, "unable to warn user", "code", v.Code)
  797. default:
  798. return s.runtimeErr(ctx, errors.New("unexpected response"))
  799. }
  800. return ""
  801. }
  802. // FormatNickname handles the toc_format_nickname TOC command.
  803. //
  804. // From the TiK documentation:
  805. //
  806. // Reformat a user's nickname. An ADMIN_NICK_STATUS or ERROR message will be
  807. // sent back to the client.
  808. //
  809. // Command syntax: toc_format_nickname <new_format>
  810. func (s OSCARProxy) FormatNickname(ctx context.Context, me *state.Session, args []byte) string {
  811. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Admin, wire.AdminInfoChangeRequest); isLimited {
  812. return errMsg
  813. }
  814. var newFormat string
  815. if _, err := parseArgs(args, &newFormat); err != nil {
  816. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  817. }
  818. // remove curly braces added by TiK
  819. newFormat = strings.Trim(newFormat, "{}")
  820. reqSNAC := wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  821. TLVRestBlock: wire.TLVRestBlock{
  822. TLVList: wire.TLVList{
  823. wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, newFormat),
  824. },
  825. },
  826. }
  827. reply, err := s.AdminService.InfoChangeRequest(ctx, me, wire.SNACFrame{}, reqSNAC)
  828. if err != nil {
  829. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: %w", err))
  830. }
  831. replyBody, ok := reply.Body.(wire.SNAC_0x07_0x05_AdminChangeReply)
  832. if !ok {
  833. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", replyBody))
  834. }
  835. code, ok := replyBody.Uint16BE(wire.AdminTLVErrorCode)
  836. if ok {
  837. switch code {
  838. case wire.AdminInfoErrorInvalidNickNameLength, wire.AdminInfoErrorInvalidNickName:
  839. return "ERROR:911"
  840. default:
  841. return "ERROR:913"
  842. }
  843. }
  844. return "ADMIN_NICK_STATUS:0"
  845. }
  846. // GetDirSearchURL handles the toc_dir_search TOC command.
  847. //
  848. // From the TiK documentation:
  849. //
  850. // Perform a search of the Oscar Directory, using colon separated fields as in:
  851. //
  852. // "first name":"middle name":"last name":"maiden name":"city":"state":"country":"email"
  853. //
  854. // You can search by keyword by setting search terms in the 11th position (this
  855. // feature is not in the TiK docs but is present in the code):
  856. //
  857. // ::::::::::"search kw"
  858. //
  859. // Returns either a GOTO_URL or ERROR msg.
  860. //
  861. // Command syntax: toc_dir_search <info information>
  862. func (s OSCARProxy) GetDirSearchURL(ctx context.Context, me *state.Session, args []byte) string {
  863. if status := me.EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  864. return rateLimitExceededErr
  865. }
  866. var info string
  867. if _, err := parseArgs(args, &info); err != nil {
  868. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  869. }
  870. info = unescape(info)
  871. params := strings.Split(info, ":")
  872. labels := []string{
  873. "first_name",
  874. "middle_name",
  875. "last_name",
  876. "maiden_name",
  877. "city",
  878. "state",
  879. "country",
  880. "email",
  881. "nop", // unused placeholder
  882. "nop",
  883. "keyword",
  884. }
  885. // map labels to param values at their corresponding positions
  886. p := url.Values{}
  887. for i, param := range params {
  888. if i >= len(labels) {
  889. break
  890. }
  891. if param != "" {
  892. p.Add(labels[i], strings.Trim(param, "\""))
  893. }
  894. }
  895. if len(p) == 0 {
  896. return s.runtimeErr(ctx, errors.New("no search fields found"))
  897. }
  898. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  899. if err != nil {
  900. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  901. }
  902. p.Add("cookie", cookie)
  903. return fmt.Sprintf("GOTO_URL:search results:dir_search?%s", p.Encode())
  904. }
  905. // GetDirURL handles the toc_get_dir TOC command.
  906. //
  907. // From the TiK documentation:
  908. //
  909. // Gets a user's dir info a GOTO_URL or ERROR message will be sent back to the client.
  910. //
  911. // Command syntax: toc_get_dir <username>
  912. func (s OSCARProxy) GetDirURL(ctx context.Context, me *state.Session, args []byte) string {
  913. if status := me.EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  914. return rateLimitExceededErr
  915. }
  916. var user string
  917. if _, err := parseArgs(args, &user); err != nil {
  918. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  919. }
  920. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  921. if err != nil {
  922. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  923. }
  924. p := url.Values{}
  925. p.Add("cookie", cookie)
  926. p.Add("user", user)
  927. return fmt.Sprintf("GOTO_URL:directory info:dir_info?%s", p.Encode())
  928. }
  929. // GetInfoURL handles the toc_get_info TOC command.
  930. //
  931. // From the TiK documentation:
  932. //
  933. // Gets a user's info a GOTO_URL or ERROR message will be sent back to the client.
  934. //
  935. // Command syntax: toc_get_info <username>
  936. func (s OSCARProxy) GetInfoURL(ctx context.Context, me *state.Session, args []byte) string {
  937. if status := me.EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  938. return rateLimitExceededErr
  939. }
  940. var user string
  941. if _, err := parseArgs(args, &user); err != nil {
  942. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  943. }
  944. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  945. if err != nil {
  946. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  947. }
  948. p := url.Values{}
  949. p.Add("cookie", cookie)
  950. p.Add("from", me.IdentScreenName().String())
  951. p.Add("user", user)
  952. return fmt.Sprintf("GOTO_URL:profile:info?%s", p.Encode())
  953. }
  954. // GetStatus handles the toc_get_status TOC command.
  955. //
  956. // From the TOC2 documentation:
  957. //
  958. // This useful command wasn't ever really documented. It returns either an
  959. // UPDATE_BUDDY message or an ERROR message depending on whether or not the
  960. // guy appears to be online.
  961. //
  962. // Command syntax: toc_get_status <screenname>
  963. func (s OSCARProxy) GetStatus(ctx context.Context, me *state.Session, args []byte) string {
  964. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateUserInfoQuery); isLimited {
  965. return errMsg
  966. }
  967. var them string
  968. if _, err := parseArgs(args, &them); err != nil {
  969. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  970. }
  971. inBody := wire.SNAC_0x02_0x05_LocateUserInfoQuery{
  972. ScreenName: them,
  973. }
  974. info, err := s.LocateService.UserInfoQuery(ctx, me, wire.SNACFrame{}, inBody)
  975. if err != nil {
  976. return s.runtimeErr(ctx, fmt.Errorf("LocateService.UserInfoQuery: %w", err))
  977. }
  978. switch v := info.Body.(type) {
  979. case wire.SNACError:
  980. if v.Code == wire.ErrorCodeNotLoggedOn {
  981. return fmt.Sprintf("ERROR:901:%s", them)
  982. } else {
  983. return s.runtimeErr(ctx, fmt.Errorf("LocateService.UserInfoQuery error code: %d", v.Code))
  984. }
  985. case wire.SNAC_0x02_0x06_LocateUserInfoReply:
  986. return userInfoToUpdateBuddy(v.TLVUserInfo)
  987. default:
  988. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", v))
  989. }
  990. }
  991. // InitDone handles the toc_init_done TOC command.
  992. //
  993. // From the TiK documentation:
  994. //
  995. // Tells TOC that we are ready to go online. TOC clients should first send TOC
  996. // the buddy list and any permit/deny lists. However, toc_init_done must be
  997. // called within 30 seconds after toc_signon, or the connection will be
  998. // dropped. Remember, it can't be called until after the SIGN_ON message is
  999. // received. Calling this before or multiple times after a SIGN_ON will cause
  1000. // the connection to be dropped.
  1001. //
  1002. // Note: The business logic described in the last 3 sentences are not yet
  1003. // implemented.
  1004. //
  1005. // Command syntax: toc_init_done
  1006. func (s OSCARProxy) InitDone(ctx context.Context, sess *state.Session) string {
  1007. if errMsg, isLimited := s.checkRateLimit(ctx, sess, wire.OService, wire.OServiceClientOnline); isLimited {
  1008. return errMsg
  1009. }
  1010. if err := s.OServiceService.ClientOnline(ctx, wire.BOS, wire.SNAC_0x01_0x02_OServiceClientOnline{}, sess); err != nil {
  1011. return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.ClientOnliney: %w", err))
  1012. }
  1013. return ""
  1014. }
  1015. // RemoveBuddy handles the toc_remove_buddy TOC command.
  1016. //
  1017. // From the TiK documentation:
  1018. //
  1019. // Remove buddies from your buddy list. This does not change your saved config.
  1020. //
  1021. // Command syntax: toc_remove_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
  1022. func (s OSCARProxy) RemoveBuddy(ctx context.Context, me *state.Session, args []byte) string {
  1023. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Buddy, wire.BuddyDelBuddies); isLimited {
  1024. return errMsg
  1025. }
  1026. users, err := parseArgs(args)
  1027. if err != nil {
  1028. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1029. }
  1030. snac := wire.SNAC_0x03_0x05_BuddyDelBuddies{}
  1031. for _, sn := range users {
  1032. snac.Buddies = append(snac.Buddies, struct {
  1033. ScreenName string `oscar:"len_prefix=uint8"`
  1034. }{ScreenName: sn})
  1035. }
  1036. if err := s.BuddyService.DelBuddies(ctx, me, snac); err != nil {
  1037. return s.runtimeErr(ctx, fmt.Errorf("BuddyService.DelBuddies: %w", err))
  1038. }
  1039. return ""
  1040. }
  1041. // RvousAccept handles the toc_rvous_accept TOC command.
  1042. //
  1043. // From the TiK documentation:
  1044. //
  1045. // Accept a rendezvous proposal from the user <nick>. <cookie> is the cookie
  1046. // from the RVOUS_PROPOSE message. <service> is the UUID the proposal was for.
  1047. // <tlvlist> contains a list of tlv tags followed by base64 encoded values.
  1048. //
  1049. // Note: This method does not actually process the TLV list param, as it's not
  1050. // passed in the TiK client, the reference implementation.
  1051. //
  1052. // Command syntax: toc_rvous_accept <nick> <cookie> <service>
  1053. func (s OSCARProxy) RvousAccept(ctx context.Context, me *state.Session, args []byte) string {
  1054. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  1055. return errMsg
  1056. }
  1057. var nick, cookie, service string
  1058. if _, err := parseArgs(args, &nick, &cookie, &service); err != nil {
  1059. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1060. }
  1061. cbytes, err := base64.StdEncoding.DecodeString(cookie)
  1062. if err != nil {
  1063. return s.runtimeErr(ctx, fmt.Errorf("base64.Decode: %w", err))
  1064. }
  1065. var arr [8]byte
  1066. copy(arr[:], cbytes) // copy slice into array
  1067. svcUUID, err := uuid.Parse(service)
  1068. if err != nil {
  1069. return s.runtimeErr(ctx, fmt.Errorf("uuid.Parse: %w", err))
  1070. }
  1071. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  1072. ChannelID: wire.ICBMChannelRendezvous,
  1073. ScreenName: nick,
  1074. TLVRestBlock: wire.TLVRestBlock{
  1075. TLVList: wire.TLVList{
  1076. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  1077. Type: wire.ICBMRdvMessageAccept,
  1078. Cookie: arr,
  1079. Capability: svcUUID,
  1080. }),
  1081. },
  1082. },
  1083. }
  1084. if _, err = s.ICBMService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  1085. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  1086. }
  1087. return ""
  1088. }
  1089. // RvousCancel handles the toc_rvous_cancel TOC command.
  1090. //
  1091. // From the TiK documentation:
  1092. //
  1093. // Cancel a rendezvous proposal from the user <nick>. <cookie> is the cookie
  1094. // from the RVOUS_PROPOSE message. <service> is the UUID the proposal was for.
  1095. // <tlvlist> contains a list of tlv tags followed by base64 encoded values.
  1096. //
  1097. // Note: This method does not actually process the TLV list param, as it's not
  1098. // passed in the TiK client, the reference implementation.
  1099. //
  1100. // Command syntax: toc_rvous_cancel <nick> <cookie> <service>
  1101. func (s OSCARProxy) RvousCancel(ctx context.Context, me *state.Session, args []byte) string {
  1102. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  1103. return errMsg
  1104. }
  1105. var nick, cookie, service string
  1106. if _, err := parseArgs(args, &nick, &cookie, &service); err != nil {
  1107. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1108. }
  1109. cbytes, err := base64.StdEncoding.DecodeString(cookie)
  1110. if err != nil {
  1111. return s.runtimeErr(ctx, fmt.Errorf("base64.Decode: %w", err))
  1112. }
  1113. var arr [8]byte
  1114. copy(arr[:], cbytes) // copy slice into array
  1115. svcUUID, err := uuid.Parse(service)
  1116. if err != nil {
  1117. return s.runtimeErr(ctx, fmt.Errorf("uuid.Parse: %w", err))
  1118. }
  1119. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  1120. ChannelID: wire.ICBMChannelRendezvous,
  1121. ScreenName: nick,
  1122. TLVRestBlock: wire.TLVRestBlock{
  1123. TLVList: wire.TLVList{
  1124. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  1125. Type: wire.ICBMRdvMessageCancel,
  1126. Cookie: arr,
  1127. Capability: svcUUID,
  1128. TLVRestBlock: wire.TLVRestBlock{
  1129. TLVList: wire.TLVList{
  1130. wire.NewTLVBE(wire.ICBMRdvTLVTagsCancelReason, wire.ICBMRdvCancelReasonsUserCancel),
  1131. },
  1132. },
  1133. }),
  1134. },
  1135. },
  1136. }
  1137. if _, err = s.ICBMService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  1138. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  1139. }
  1140. return ""
  1141. }
  1142. // SendIM handles the toc_send_im TOC command.
  1143. //
  1144. // From the TiK documentation:
  1145. //
  1146. // Send a message to a remote user. Remember to quote and encode the message.
  1147. // If the optional string "auto" is the last argument, then the auto response
  1148. // flag will be turned on for the IM.
  1149. //
  1150. // Command syntax: toc_send_im <Destination User> <Message> [auto]
  1151. func (s OSCARProxy) SendIM(ctx context.Context, sender *state.Session, args []byte) string {
  1152. if msg, isLimited := s.checkRateLimit(ctx, sender, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  1153. return msg
  1154. }
  1155. var recip, msg string
  1156. autoReply, err := parseArgs(args, &recip, &msg)
  1157. if err != nil {
  1158. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1159. }
  1160. msg = unescape(msg)
  1161. frags, err := wire.ICBMFragmentList(msg)
  1162. if err != nil {
  1163. return s.runtimeErr(ctx, fmt.Errorf("wire.ICBMFragmentList: %w", err))
  1164. }
  1165. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  1166. ChannelID: wire.ICBMChannelIM,
  1167. ScreenName: recip,
  1168. TLVRestBlock: wire.TLVRestBlock{
  1169. TLVList: wire.TLVList{
  1170. wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags),
  1171. },
  1172. },
  1173. }
  1174. if len(autoReply) > 0 && autoReply[0] == "auto" {
  1175. snac.Append(wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}))
  1176. }
  1177. // send message and ignore response since there is no TOC error code to
  1178. // handle errors such as "user is offline", etc.
  1179. _, err = s.ICBMService.ChannelMsgToHost(ctx, sender, wire.SNACFrame{}, snac)
  1180. if err != nil {
  1181. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  1182. }
  1183. return ""
  1184. }
  1185. // SetAway handles the toc_chat_join TOC command.
  1186. //
  1187. // From the TiK documentation:
  1188. //
  1189. // If the away message is present, then the unavailable status flag is set for
  1190. // the user. If the away message is not present, then the unavailable status
  1191. // flag is unset. The away message is basic HTML, remember to encode the
  1192. // information.
  1193. //
  1194. // Command syntax: toc_set_away [<away message>]
  1195. func (s OSCARProxy) SetAway(ctx context.Context, me *state.Session, args []byte) string {
  1196. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
  1197. return errMsg
  1198. }
  1199. maybeMsg, err := parseArgs(args)
  1200. if err != nil {
  1201. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1202. }
  1203. var msg string
  1204. if len(maybeMsg) > 0 {
  1205. msg = unescape(maybeMsg[0])
  1206. }
  1207. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1208. TLVRestBlock: wire.TLVRestBlock{
  1209. TLVList: wire.TLVList{
  1210. wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableData, msg),
  1211. },
  1212. },
  1213. }
  1214. if err := s.LocateService.SetInfo(ctx, me, snac); err != nil {
  1215. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
  1216. }
  1217. return ""
  1218. }
  1219. // SetCaps handles the toc_set_caps TOC command.
  1220. //
  1221. // From the TiK documentation:
  1222. //
  1223. // Set my capabilities. All capabilities that we support need to be sent at
  1224. // the same time. Capabilities are represented by UUIDs.
  1225. //
  1226. // This method automatically adds the "chat" capability since it doesn't seem
  1227. // to be sent explicitly by the official clients, even though they support
  1228. // chat.
  1229. //
  1230. // Command syntax: toc_set_caps [ <Capability 1> [<Capability 2> [...]]]
  1231. func (s OSCARProxy) SetCaps(ctx context.Context, me *state.Session, args []byte) string {
  1232. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
  1233. return errMsg
  1234. }
  1235. params, err := parseArgs(args)
  1236. if err != nil {
  1237. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1238. }
  1239. caps := make([]uuid.UUID, 0, 16*(len(params)+1))
  1240. for _, capStr := range params {
  1241. uid, err := uuid.Parse(capStr)
  1242. if err != nil {
  1243. return s.runtimeErr(ctx, fmt.Errorf("UUID.Parse: %w", err))
  1244. }
  1245. caps = append(caps, uid)
  1246. }
  1247. // assume client supports chat, although we may want to do this according
  1248. // to client ID
  1249. caps = append(caps, wire.CapChat)
  1250. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1251. TLVRestBlock: wire.TLVRestBlock{
  1252. TLVList: wire.TLVList{
  1253. wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, caps),
  1254. },
  1255. },
  1256. }
  1257. if err := s.LocateService.SetInfo(ctx, me, snac); err != nil {
  1258. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
  1259. }
  1260. return ""
  1261. }
  1262. // SetConfig handles the toc_set_config TOC command.
  1263. //
  1264. // From the TiK documentation:
  1265. //
  1266. // Set the config information for this user. The config information is line
  1267. // oriented with the first character being the item type, followed by a space,
  1268. // with the rest of the line being the item value. Only letters, numbers, and
  1269. // spaces should be used. Remember you will have to enclose the entire config
  1270. // in quotes.
  1271. //
  1272. // Item Types:
  1273. // - g - Buddy Group (All Buddies until the next g or the end of config are in this group.)
  1274. // - b - A Buddy
  1275. // - p - Person on permit list
  1276. // - d - Person on deny list
  1277. // - m - Permit/Deny Mode. Possible values are
  1278. // - 1 - Permit All
  1279. // - 2 - Deny All
  1280. // - 3 - Permit Some
  1281. // - 4 - Deny Some
  1282. //
  1283. // This method doesn't attempt to validate any of the configuration--it saves
  1284. // the config as received from the client.
  1285. //
  1286. // Command syntax: toc_set_config <Config Info>
  1287. func (s OSCARProxy) SetConfig(ctx context.Context, me *state.Session, args []byte) string {
  1288. if status := me.EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  1289. return rateLimitExceededErr
  1290. }
  1291. // most TOC clients don't quote the config info argument, despite what the
  1292. // documentation specifies. this makes the argument payload incompatible
  1293. // for CSV parsing. since this command takes a single argument, we can get
  1294. // away with trimming quotes and spaces from the byte slice before passing
  1295. // it to the config store.
  1296. args = bytes.Trim(args, "'\" ")
  1297. config := string(args)
  1298. if config == "" {
  1299. return s.runtimeErr(ctx, fmt.Errorf("empty config"))
  1300. }
  1301. if err := s.TOCConfigStore.SetTOCConfig(ctx, me.IdentScreenName(), config); err != nil {
  1302. return s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.SaveTOCConfig: %w", err))
  1303. }
  1304. return ""
  1305. }
  1306. // SetDir handles the toc_set_dir TOC command.
  1307. //
  1308. // From the TiK documentation:
  1309. //
  1310. // Set the DIR user information. This is a colon separated fields as in:
  1311. //
  1312. // "first name":"middle name":"last name":"maiden name":"city":"state":"country":"email":"allow web searches".
  1313. //
  1314. // Should return a DIR_STATUS msg. Having anything in the "allow web searches"
  1315. // field allows people to use web-searches to find your directory info.
  1316. // Otherwise, they'd have to use the client.
  1317. //
  1318. // The fields "email" and "allow web searches" are ignored by this method.
  1319. //
  1320. // Command syntax: toc_set_dir <info information>
  1321. func (s OSCARProxy) SetDir(ctx context.Context, me *state.Session, args []byte) string {
  1322. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetDirInfo); isLimited {
  1323. return errMsg
  1324. }
  1325. var info string
  1326. if _, err := parseArgs(args, &info); err != nil {
  1327. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1328. }
  1329. info = unescape(info)
  1330. rawFields := strings.Split(info, ":")
  1331. var finalFields [9]string
  1332. if len(rawFields) > len(finalFields) {
  1333. return s.runtimeErr(ctx, fmt.Errorf("expected at most %d params, got %d", len(finalFields), len(rawFields)))
  1334. }
  1335. for i, a := range rawFields {
  1336. finalFields[i] = strings.Trim(a, "\"")
  1337. }
  1338. snac := wire.SNAC_0x02_0x09_LocateSetDirInfo{
  1339. TLVRestBlock: wire.TLVRestBlock{
  1340. TLVList: wire.TLVList{
  1341. wire.NewTLVBE(wire.ODirTLVFirstName, finalFields[0]),
  1342. wire.NewTLVBE(wire.ODirTLVMiddleName, finalFields[1]),
  1343. wire.NewTLVBE(wire.ODirTLVLastName, finalFields[2]),
  1344. wire.NewTLVBE(wire.ODirTLVMaidenName, finalFields[3]),
  1345. wire.NewTLVBE(wire.ODirTLVCountry, finalFields[6]),
  1346. wire.NewTLVBE(wire.ODirTLVState, finalFields[5]),
  1347. wire.NewTLVBE(wire.ODirTLVCity, finalFields[4]),
  1348. },
  1349. },
  1350. }
  1351. if _, err := s.LocateService.SetDirInfo(ctx, me, wire.SNACFrame{}, snac); err != nil {
  1352. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetDirInfo: %w", err))
  1353. }
  1354. return ""
  1355. }
  1356. // SetIdle handles the toc_set_idle TOC command.
  1357. //
  1358. // From the TiK documentation:
  1359. //
  1360. // Set idle information. If <idle secs> is 0 then the user isn't idle at all.
  1361. // If <idle secs> is greater than 0 then the user has already been idle for
  1362. // <idle secs> number of seconds. The server will automatically keep
  1363. // incrementing this number, so do not repeatedly call with new idle times.
  1364. //
  1365. // Command syntax: toc_set_idle <idle secs>
  1366. func (s OSCARProxy) SetIdle(ctx context.Context, me *state.Session, args []byte) string {
  1367. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceIdleNotification); isLimited {
  1368. return errMsg
  1369. }
  1370. var idleTimeStr string
  1371. if _, err := parseArgs(args, &idleTimeStr); err != nil {
  1372. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1373. }
  1374. time, err := strconv.Atoi(idleTimeStr)
  1375. if err != nil {
  1376. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  1377. }
  1378. snac := wire.SNAC_0x01_0x11_OServiceIdleNotification{
  1379. IdleTime: uint32(time),
  1380. }
  1381. if err := s.OServiceService.IdleNotification(ctx, me, snac); err != nil {
  1382. return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.IdleNotification: %w", err))
  1383. }
  1384. return ""
  1385. }
  1386. // SetInfo handles the toc_set_info TOC command.
  1387. //
  1388. // From the TiK documentation:
  1389. //
  1390. // Set the LOCATE user information. This is basic HTML. Remember to encode the info.
  1391. //
  1392. // Command syntax: toc_set_info <info information>
  1393. func (s OSCARProxy) SetInfo(ctx context.Context, me *state.Session, args []byte) string {
  1394. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
  1395. return errMsg
  1396. }
  1397. var info string
  1398. if _, err := parseArgs(args, &info); err != nil {
  1399. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1400. }
  1401. info = unescape(info)
  1402. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1403. TLVRestBlock: wire.TLVRestBlock{
  1404. TLVList: wire.TLVList{
  1405. wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, info),
  1406. },
  1407. },
  1408. }
  1409. if err := s.LocateService.SetInfo(ctx, me, snac); err != nil {
  1410. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
  1411. }
  1412. return ""
  1413. }
  1414. // Signon handles the toc_signon TOC command.
  1415. //
  1416. // From the TiK documentation:
  1417. //
  1418. // The password needs to be roasted with the Roasting String if coming over a
  1419. // FLAP connection, CP connections don't use roasted passwords. The language
  1420. // specified will be used when generating web pages, such as the get info
  1421. // pages. Currently, the only supported language is "english". If the language
  1422. // sent isn't found, the default "english" language will be used. The version
  1423. // string will be used for the client identity, and must be less than 50
  1424. // characters.
  1425. //
  1426. // Passwords are roasted when sent to the host. This is done so they aren't
  1427. // sent in "clear text" over the wire, although they are still trivial to
  1428. // decode. Roasting is performed by first xoring each byte in the password
  1429. // with the equivalent modulo byte in the roasting string. The result is then
  1430. // converted to ascii hex, and prepended with "0x". So for example the
  1431. // password "password" roasts to "0x2408105c23001130".
  1432. //
  1433. // The Roasting String is Tic/Toc.
  1434. //
  1435. // Command syntax: toc_signon <authorizer host> <authorizer port> <User Name> <Password> <language> <version>
  1436. func (s OSCARProxy) Signon(ctx context.Context, args []byte) (*state.Session, []string) {
  1437. var userName, password string
  1438. if _, err := parseArgs(args, nil, nil, &userName, &password); err != nil {
  1439. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))}
  1440. }
  1441. passwordHash, err := hex.DecodeString(password[2:])
  1442. if err != nil {
  1443. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("hex.DecodeString: %w", err))}
  1444. }
  1445. signonFrame := wire.FLAPSignonFrame{}
  1446. signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsScreenName, userName))
  1447. signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsRoastedTOCPassword, passwordHash))
  1448. block, err := s.AuthService.FLAPLogin(ctx, signonFrame, state.NewStubUser, "")
  1449. if err != nil {
  1450. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("AuthService.FLAPLogin: %w", err))}
  1451. }
  1452. if block.HasTag(wire.LoginTLVTagsErrorSubcode) {
  1453. s.Logger.DebugContext(ctx, "login failed")
  1454. return nil, []string{"ERROR:980"} // bad username/password
  1455. }
  1456. authCookie, ok := block.Bytes(wire.OServiceTLVTagsLoginCookie)
  1457. if !ok {
  1458. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("unable to get session id from payload"))}
  1459. }
  1460. // todo: naming for cookie: login cookie, server cookie, or auth cookie?
  1461. serverCookie, err := s.AuthService.CrackCookie(authCookie)
  1462. sess, err := s.AuthService.RegisterBOSSession(ctx, serverCookie)
  1463. if err != nil {
  1464. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterBOSSession: %w", err))}
  1465. }
  1466. // set chat capability so that... tk
  1467. sess.SetCaps([][16]byte{wire.CapChat})
  1468. if err := s.BuddyListRegistry.RegisterBuddyList(ctx, sess.IdentScreenName()); err != nil {
  1469. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("BuddyListRegistry.RegisterBuddyList: %w", err))}
  1470. }
  1471. u, err := s.TOCConfigStore.User(ctx, sess.IdentScreenName())
  1472. if err != nil {
  1473. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: %w", err))}
  1474. }
  1475. if u == nil {
  1476. return nil, []string{s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: user not found"))}
  1477. }
  1478. return sess, []string{"SIGN_ON:TOC1.0", fmt.Sprintf("CONFIG:%s", u.TOCConfig)}
  1479. }
  1480. // Signout terminates a TOC session. It sends departure notifications to
  1481. // buddies, de-registers buddy list and session.
  1482. func (s OSCARProxy) Signout(ctx context.Context, me *state.Session, chatRegistry *ChatRegistry) {
  1483. if err := s.BuddyService.BroadcastBuddyDeparted(ctx, me); err != nil {
  1484. s.Logger.ErrorContext(ctx, "error sending departure notifications", "err", err.Error())
  1485. }
  1486. if err := s.BuddyListRegistry.UnregisterBuddyList(ctx, me.IdentScreenName()); err != nil {
  1487. s.Logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
  1488. }
  1489. s.AuthService.Signout(ctx, me)
  1490. for _, sess := range chatRegistry.Sessions() {
  1491. s.AuthService.SignoutChat(ctx, sess)
  1492. sess.Close() // stop async server SNAC reply handler for this chat room
  1493. }
  1494. }
  1495. // newHTTPAuthToken creates a HMAC token for authenticating TOC HTTP requests
  1496. func (s OSCARProxy) newHTTPAuthToken(me state.IdentScreenName) (string, error) {
  1497. cookie, err := s.CookieBaker.Issue([]byte(me.String()))
  1498. if err != nil {
  1499. return "", err
  1500. }
  1501. // trim padding so that gaim doesn't choke on the long value
  1502. cookie = bytes.TrimRight(cookie, "\x00")
  1503. return hex.EncodeToString(cookie), nil
  1504. }
  1505. // parseArgs extracts arguments from a TOC command. Each positional argument is
  1506. // assigned to its corresponding args pointer. It returns the remaining
  1507. // arguments as varargs.
  1508. func parseArgs(payload []byte, args ...*string) (varArgs []string, err error) {
  1509. if len(payload) == 0 && len(args) == 0 {
  1510. return []string{}, nil
  1511. }
  1512. reader := csv.NewReader(bytes.NewReader(payload))
  1513. reader.Comma = ' '
  1514. reader.LazyQuotes = true
  1515. reader.TrimLeadingSpace = true
  1516. segs, err := reader.Read()
  1517. if err != nil {
  1518. return []string{}, fmt.Errorf("CSV reader error: %w", err)
  1519. }
  1520. if len(segs) < len(args) {
  1521. return []string{}, fmt.Errorf("command contains fewer arguments than expected")
  1522. }
  1523. // populate placeholder pointers with their corresponding values
  1524. for i, arg := range args {
  1525. if arg != nil {
  1526. *arg = strings.TrimSpace(segs[i])
  1527. }
  1528. }
  1529. // dump remaining arguments as varargs
  1530. return segs[len(args):], err
  1531. }
  1532. // runtimeErr is a convenience function that logs an error and returns a TOC
  1533. // internal server error.
  1534. func (s OSCARProxy) runtimeErr(ctx context.Context, err error) string {
  1535. s.Logger.ErrorContext(ctx, "internal service error", "err", err.Error())
  1536. return cmdInternalSvcErr
  1537. }
  1538. func (s OSCARProxy) checkRateLimit(ctx context.Context, sender *state.Session, foodGroup uint16, subGroup uint16) (string, bool) {
  1539. rateClassID, ok := s.SNACRateLimits.RateClassLookup(foodGroup, subGroup)
  1540. if !ok {
  1541. s.Logger.ErrorContext(ctx, "rate limit not found, allowing request through")
  1542. return "", false
  1543. }
  1544. if status := sender.EvaluateRateLimit(time.Now(), rateClassID); status == wire.RateLimitStatusLimited {
  1545. s.Logger.DebugContext(ctx, "(toc) rate limit exceeded, dropping SNAC",
  1546. "foodgroup", wire.FoodGroupName(foodGroup),
  1547. "subgroup", wire.SubGroupName(foodGroup, subGroup))
  1548. return rateLimitExceededErr, true
  1549. }
  1550. return "", false
  1551. }
  1552. // unescape removes escaping from the following TOC characters: \ { } ( ) [ ] $ "
  1553. func unescape(encoded string) string {
  1554. if !strings.ContainsRune(encoded, '\\') {
  1555. return encoded
  1556. }
  1557. var result strings.Builder
  1558. result.Grow(len(encoded))
  1559. escaped := false
  1560. for i := 0; i < len(encoded); i++ {
  1561. ch := encoded[i]
  1562. if escaped {
  1563. // append escaped character without the backslash
  1564. result.WriteByte(ch)
  1565. escaped = false
  1566. } else if ch == '\\' {
  1567. escaped = true
  1568. } else {
  1569. result.WriteByte(ch)
  1570. }
  1571. }
  1572. return result.String()
  1573. }