cmd_client.go 70 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294
  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"
  12. "net/url"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/google/uuid"
  18. "github.com/mk6i/open-oscar-server/config"
  19. "github.com/mk6i/open-oscar-server/state"
  20. "github.com/mk6i/open-oscar-server/wire"
  21. )
  22. // OSCARProxy acts as a bridge between TOC clients and the OSCAR server,
  23. // translating protocol messages between the two.
  24. //
  25. // It performs the following functions:
  26. // - Receives TOC commands from the client, converts them into SNAC
  27. // requests, and sends them to the OSCAR server (cmd_client.go).
  28. // - Receives SNACs from the OSCAR server, both replies to those requests and
  29. // server-initiated messages, and translates them into TOC responses for
  30. // the client (cmd_server.go).
  31. type OSCARProxy struct {
  32. CookieBaker CookieBaker
  33. DirSearchService DirSearchService
  34. HTTPIPRateLimiter *IPRateLimiter
  35. LocateService LocateService
  36. Logger *slog.Logger
  37. // OSCARServer serves the OSCAR connections TOC opens for its users.
  38. OSCARServer OSCARServer
  39. // OSCAREndpoint is the OSCAR listener that TOC's connections appear to
  40. // arrive on.
  41. OSCAREndpoint config.Endpoint
  42. SessionRetriever SessionRetriever
  43. TOCConfigStore TOCConfigStore
  44. // RandIntn is the source for feedbag item ID generation.
  45. // Inject a deterministic func in tests to assert exact feedbag item slices.
  46. RandIntn func(n int) int
  47. }
  48. // signonTimeout bounds how long signon waits for each OSCAR server reply.
  49. const signonTimeout = 10 * time.Second
  50. // Request contexts are cached with each SNAC that TOC sends to the OSCAR
  51. // server, under the SNAC's request ID (see snacCache). When the reply or error
  52. // comes back, cmd_server.go uses the context's type to tell which TOC command
  53. // the reply answers, and the context's fields for what the reply doesn't
  54. // carry.
  55. type (
  56. // getStatusReqCtx is the request context of a toc_get_status user info
  57. // query.
  58. getStatusReqCtx struct {
  59. screenName string
  60. }
  61. // evilReqCtx is the request context of a toc_evil warning.
  62. evilReqCtx struct{}
  63. // changePasswdReqCtx is the request context of a toc_change_passwd admin
  64. // request.
  65. changePasswdReqCtx struct{}
  66. // formatNickReqCtx is the request context of a toc_format_nickname admin
  67. // request.
  68. formatNickReqCtx struct{}
  69. // chatJoinReqCtx is the request context of the room creation that starts
  70. // toc_chat_join.
  71. chatJoinReqCtx struct{}
  72. // chatAcceptReqCtx is the request context of the room info query that
  73. // starts toc_chat_accept.
  74. chatAcceptReqCtx struct {
  75. chatID int
  76. }
  77. // chatServiceReqCtx is the request context of a request for a chat room
  78. // connection.
  79. chatServiceReqCtx struct {
  80. chatID int
  81. roomName string
  82. }
  83. // feedbagChangeReqCtx is the request context of a feedbag change.
  84. feedbagChangeReqCtx struct{}
  85. // feedbagReloadReqCtx is the request context of a feedbag query that
  86. // reloads the feedbag mirror. marker identifies the changes sent before
  87. // the query.
  88. feedbagReloadReqCtx struct {
  89. marker uint64
  90. }
  91. )
  92. // RecvClientCmd processes a client TOC command and returns the replies that
  93. // don't depend on a response from the OSCAR server. Replies that do are sent
  94. // by cmd_server.go once the response arrives.
  95. //
  96. // * me is the current user's TOC connection.
  97. // * payload is the command + arguments
  98. func (s OSCARProxy) RecvClientCmd(ctx context.Context, me *tocConn, payload []byte) (replies []string) {
  99. cmd := payload
  100. var args []byte
  101. if idx := bytes.IndexByte(payload, ' '); idx > -1 {
  102. cmd, args = payload[:idx], payload[idx:]
  103. }
  104. if s.Logger.Enabled(ctx, slog.LevelDebug) {
  105. s.Logger.DebugContext(ctx, "client request", "command", payload)
  106. } else {
  107. s.Logger.InfoContext(ctx, "client request", "command", cmd)
  108. }
  109. if bytes.HasPrefix(cmd, []byte("toc2_")) && !me.IsTOC2() {
  110. s.Logger.ErrorContext(ctx, fmt.Sprintf("TOC2 command %s from a TOC1 session", cmd))
  111. return []string{cmdInternalSvcErr}
  112. }
  113. switch string(cmd) {
  114. case "toc_send_im", "toc2_send_im":
  115. return s.SendIM(ctx, me, args)
  116. case "toc_init_done":
  117. return s.InitDone(ctx, me)
  118. case "toc_add_buddy":
  119. return s.AddBuddy(ctx, me, args)
  120. case "toc_get_status":
  121. return s.GetStatus(ctx, me, args)
  122. case "toc_remove_buddy":
  123. return s.RemoveBuddy(ctx, me, args)
  124. case "toc_add_permit":
  125. return s.AddPermit(ctx, me, args)
  126. case "toc_add_deny":
  127. return s.AddDeny(ctx, me, args)
  128. case "toc_set_away":
  129. return s.SetAway(ctx, me, args)
  130. case "toc_set_caps":
  131. return s.SetCaps(ctx, me, args)
  132. case "toc_evil":
  133. return s.Evil(ctx, me, args)
  134. case "toc_get_info":
  135. return s.GetInfoURL(ctx, me, args)
  136. case "toc_change_passwd":
  137. return s.ChangePassword(ctx, me, args)
  138. case "toc_format_nickname":
  139. return s.FormatNickname(ctx, me, args)
  140. case "toc_chat_join":
  141. return s.ChatJoin(ctx, me, args)
  142. case "toc_chat_accept":
  143. return s.ChatAccept(ctx, me, args)
  144. case "toc_chat_send":
  145. return s.ChatSend(ctx, me, args)
  146. case "toc_chat_whisper":
  147. return s.ChatWhisper(ctx, me, args)
  148. case "toc_chat_leave":
  149. return s.ChatLeave(ctx, me, args)
  150. case "toc_set_info":
  151. return s.SetInfo(ctx, me, args)
  152. case "toc_set_dir":
  153. return s.SetDir(ctx, me, args)
  154. case "toc_set_idle":
  155. return s.SetIdle(ctx, me, args)
  156. case "toc_set_config":
  157. return s.SetConfig(ctx, me, args)
  158. case "toc_chat_invite":
  159. return s.ChatInvite(ctx, me, args)
  160. case "toc_dir_search":
  161. return s.GetDirSearchURL(ctx, me, args)
  162. case "toc_get_dir":
  163. return s.GetDirURL(ctx, me, args)
  164. case "toc_rvous_accept":
  165. return s.RvousAccept(ctx, me, args)
  166. case "toc_rvous_cancel":
  167. return s.RvousCancel(ctx, me, args)
  168. case "toc2_set_pdmode":
  169. return s.SetPDMode(ctx, me, args)
  170. case "toc2_send_im_enc":
  171. return s.SendIMEnc(ctx, me, args)
  172. case "toc2_remove_buddy":
  173. return s.RemoveBuddy2(ctx, me, args)
  174. case "toc2_new_group":
  175. return s.NewGroup(ctx, me, args)
  176. case "toc2_del_group":
  177. return s.DelGroup(ctx, me, args)
  178. case "toc2_new_buddies":
  179. return s.NewBuddies(ctx, me, args)
  180. case "toc2_add_permit":
  181. return s.AddPermit2(ctx, me, args)
  182. case "toc2_remove_permit":
  183. return s.RemovePermit2(ctx, me, args)
  184. case "toc2_add_deny":
  185. return s.AddDeny2(ctx, me, args)
  186. case "toc2_remove_deny":
  187. return s.RemoveDeny2(ctx, me, args)
  188. case "toc2_client_event":
  189. return s.SendClientEvent(ctx, me, args)
  190. }
  191. s.Logger.ErrorContext(ctx, fmt.Sprintf("unsupported TOC command %s", cmd))
  192. return []string{cmdInternalSvcErr}
  193. }
  194. // send sends a SNAC request to the OSCAR server over conn. The request is
  195. // cached by request ID along with data so that cmd_server.go can handle the
  196. // reply. It returns a TOC error if the SNAC could not be sent.
  197. func (s OSCARProxy) send(ctx context.Context, me *tocConn, conn oscarConn, foodGroup uint16, subGroup uint16, body any, data any) []string {
  198. frame := wire.SNACFrame{
  199. FoodGroup: foodGroup,
  200. SubGroup: subGroup,
  201. }
  202. me.cache.add(&frame, data)
  203. return s.sendFrame(ctx, me, conn, frame, body)
  204. }
  205. // sendFrame sends a SNAC request whose frame is already cached. It returns a
  206. // TOC error if the SNAC could not be sent.
  207. func (s OSCARProxy) sendFrame(ctx context.Context, me *tocConn, conn oscarConn, frame wire.SNACFrame, body any) []string {
  208. if err := conn.SendSNAC(frame, body); err != nil {
  209. me.cache.remove(frame.RequestID)
  210. return s.runtimeErr(ctx, fmt.Errorf("SendSNAC(%s, %s): %w",
  211. wire.FoodGroupName(frame.FoodGroup), wire.SubGroupName(frame.FoodGroup, frame.SubGroup), err))
  212. }
  213. return nil
  214. }
  215. // AddBuddy handles the toc_add_buddy TOC command.
  216. //
  217. // From the TiK documentation:
  218. //
  219. // Add buddies to your buddy list. This does not change your saved config.
  220. //
  221. // Command syntax: toc_add_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
  222. func (s OSCARProxy) AddBuddy(ctx context.Context, me *tocConn, args []byte) []string {
  223. users, err := parseArgs(args)
  224. if err != nil {
  225. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  226. }
  227. snac := wire.SNAC_0x03_0x04_BuddyAddBuddies{}
  228. for _, sn := range users {
  229. snac.Buddies = append(snac.Buddies, struct {
  230. ScreenName string `oscar:"len_prefix=uint8"`
  231. }{ScreenName: sn})
  232. }
  233. if msg := s.send(ctx, me, me.bos, wire.Buddy, wire.BuddyAddBuddies, snac, nil); msg != nil {
  234. return msg
  235. }
  236. return []string{}
  237. }
  238. // AddPermit handles the toc_add_permit TOC command.
  239. //
  240. // From the TiK documentation:
  241. //
  242. // ADD the following people to your permit mode. If you are in deny mode it
  243. // will switch you to permit mode first. With no arguments and in deny mode
  244. // this will switch you to permit none. If already in permit mode, no
  245. // arguments does nothing and your permit list remains the same.
  246. //
  247. // Command syntax: toc_add_permit [ <User 1> [<User 2> [...]]]
  248. func (s OSCARProxy) AddPermit(ctx context.Context, me *tocConn, args []byte) []string {
  249. users, err := parseArgs(args)
  250. if err != nil {
  251. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  252. }
  253. snac := wire.SNAC_0x09_0x05_PermitDenyAddPermListEntries{}
  254. for _, sn := range users {
  255. snac.Users = append(snac.Users, struct {
  256. ScreenName string `oscar:"len_prefix=uint8"`
  257. }{ScreenName: sn})
  258. }
  259. if msg := s.send(ctx, me, me.bos, wire.PermitDeny, wire.PermitDenyAddPermListEntries, snac, nil); msg != nil {
  260. return msg
  261. }
  262. return []string{}
  263. }
  264. // AddDeny handles the toc_add_deny TOC command.
  265. //
  266. // From the TiK documentation:
  267. //
  268. // ADD the following people to your deny mode. If you are in permit mode it
  269. // will switch you to deny mode first. With no arguments and in permit mode,
  270. // this will switch you to deny none. If already in deny mode, no arguments
  271. // does nothing and your deny list remains unchanged.
  272. //
  273. // Command syntax: toc_add_deny [ <User 1> [<User 2> [...]]]
  274. func (s OSCARProxy) AddDeny(ctx context.Context, me *tocConn, args []byte) []string {
  275. users, err := parseArgs(args)
  276. if err != nil {
  277. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  278. }
  279. snac := wire.SNAC_0x09_0x07_PermitDenyAddDenyListEntries{}
  280. for _, sn := range users {
  281. snac.Users = append(snac.Users, struct {
  282. ScreenName string `oscar:"len_prefix=uint8"`
  283. }{ScreenName: sn})
  284. }
  285. if msg := s.send(ctx, me, me.bos, wire.PermitDeny, wire.PermitDenyAddDenyListEntries, snac, nil); msg != nil {
  286. return msg
  287. }
  288. return []string{}
  289. }
  290. // ChangePassword handles the toc_change_passwd TOC command.
  291. //
  292. // From the TiK documentation:
  293. //
  294. // Change a user's password. An ADMIN_PASSWD_STATUS or ERROR message will be
  295. // sent back to the client.
  296. //
  297. // The reply is sent by AdminChangeReply.
  298. //
  299. // Command syntax: toc_change_passwd <existing_passwd> <new_passwd>
  300. func (s OSCARProxy) ChangePassword(ctx context.Context, me *tocConn, args []byte) []string {
  301. var oldPass, newPass string
  302. if _, err := parseArgs(args, &oldPass, &newPass); err != nil {
  303. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  304. }
  305. oldPass = unescape(oldPass)
  306. newPass = unescape(newPass)
  307. snac := wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  308. TLVRestBlock: wire.TLVRestBlock{
  309. TLVList: wire.TLVList{
  310. wire.NewTLVBE(wire.AdminTLVOldPassword, oldPass),
  311. wire.NewTLVBE(wire.AdminTLVNewPassword, newPass),
  312. },
  313. },
  314. }
  315. if msg := s.send(ctx, me, me.bos, wire.Admin, wire.AdminInfoChangeRequest, snac, changePasswdReqCtx{}); msg != nil {
  316. return msg
  317. }
  318. return []string{}
  319. }
  320. // ChatAccept handles the toc_chat_accept TOC command.
  321. //
  322. // From the TiK documentation:
  323. //
  324. // Accept a CHAT_INVITE message from TOC. The server will send a CHAT_JOIN in
  325. // response.
  326. //
  327. // This command looks up the invited room. ChatNavInfo continues the join once
  328. // the room info arrives.
  329. //
  330. // Command syntax: toc_chat_accept <Chat Room ID>
  331. func (s OSCARProxy) ChatAccept(ctx context.Context, me *tocConn, args []byte) []string {
  332. var chatIDStr string
  333. if _, err := parseArgs(args, &chatIDStr); err != nil {
  334. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  335. }
  336. chatID, err := strconv.Atoi(chatIDStr)
  337. if err != nil {
  338. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  339. }
  340. chatInfo, found := me.chats.LookupRoom(chatID)
  341. if !found {
  342. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.LookupRoom: no chat found for ID %d", chatID))
  343. }
  344. snac := wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
  345. Cookie: chatInfo.Cookie,
  346. Exchange: chatInfo.Exchange,
  347. InstanceNumber: chatInfo.Instance,
  348. }
  349. if msg := s.send(ctx, me, me.bos, wire.ChatNav, wire.ChatNavRequestRoomInfo, snac, chatAcceptReqCtx{chatID: chatID}); msg != nil {
  350. return msg
  351. }
  352. return []string{}
  353. }
  354. // ChatInvite handles the toc_chat_invite TOC command.
  355. //
  356. // From the TiK documentation:
  357. //
  358. // Once you are inside a chat room you can invite other people into that room.
  359. // Remember to quote and encode the invite message.
  360. //
  361. // Command syntax: toc_chat_invite <Chat Room ID> <Invite Msg> <buddy1> [<buddy2> [<buddy3> [...]]]
  362. func (s OSCARProxy) ChatInvite(ctx context.Context, me *tocConn, args []byte) []string {
  363. var chatRoomIDStr, msg string
  364. users, err := parseArgs(args, &chatRoomIDStr, &msg)
  365. if err != nil {
  366. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  367. }
  368. msg = unescape(msg)
  369. chatID, err := strconv.Atoi(chatRoomIDStr)
  370. if err != nil {
  371. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  372. }
  373. roomInfo, found := me.chats.LookupRoom(chatID)
  374. if !found {
  375. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.LookupRoom: chat ID `%d` not found", chatID))
  376. }
  377. for _, guest := range users {
  378. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  379. ChannelID: wire.ICBMChannelRendezvous,
  380. ScreenName: guest,
  381. TLVRestBlock: wire.TLVRestBlock{
  382. TLVList: wire.TLVList{
  383. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  384. Type: wire.ICBMRdvMessagePropose,
  385. Capability: wire.CapChat,
  386. TLVRestBlock: wire.TLVRestBlock{
  387. TLVList: wire.TLVList{
  388. wire.NewTLVBE(wire.ICBMRdvTLVTagsSeqNum, uint16(1)),
  389. wire.NewTLVBE(wire.ICBMRdvTLVTagsInvitation, msg),
  390. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMECharset, "us-ascii"),
  391. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMELang, "en"),
  392. wire.NewTLVBE(wire.ICBMRdvTLVTagsSvcData, roomInfo),
  393. },
  394. },
  395. }),
  396. },
  397. },
  398. }
  399. if msg := s.send(ctx, me, me.bos, wire.ICBM, wire.ICBMChannelMsgToHost, snac, nil); msg != nil {
  400. return msg
  401. }
  402. }
  403. return []string{}
  404. }
  405. // ChatJoin handles the toc_chat_join TOC command.
  406. //
  407. // From the TiK documentation:
  408. //
  409. // Join a chat room in the given exchange. Exchange is an integer that
  410. // represents a group of chat rooms. Different exchanges have different
  411. // properties. For example some exchanges might have room replication (ie a
  412. // room never fills up, there are just multiple instances.) and some exchanges
  413. // might have navigational information. Currently, exchange should always be
  414. // 4, however this may change in the future. You will either receive an ERROR
  415. // if the room couldn't be joined or a CHAT_JOIN message. The Chat Room Name
  416. // is case-insensitive and consecutive spaces are removed.
  417. //
  418. // This command creates the room, or retrieves it if it already exists.
  419. // ChatNavInfo continues the join once the room info arrives.
  420. //
  421. // Command syntax: toc_chat_join <Exchange> <Chat Room Name>
  422. func (s OSCARProxy) ChatJoin(ctx context.Context, me *tocConn, args []byte) []string {
  423. var exchangeStr, roomName string
  424. if _, err := parseArgs(args, &exchangeStr, &roomName); err != nil {
  425. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  426. }
  427. roomName = unescape(roomName)
  428. exchange, err := strconv.Atoi(exchangeStr)
  429. if err != nil {
  430. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  431. }
  432. snac := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  433. Exchange: uint16(exchange),
  434. Cookie: "create",
  435. TLVBlock: wire.TLVBlock{
  436. TLVList: wire.TLVList{
  437. wire.NewTLVBE(wire.ChatRoomTLVRoomName, roomName),
  438. },
  439. },
  440. }
  441. if msg := s.send(ctx, me, me.bos, wire.ChatNav, wire.ChatNavCreateRoom, snac, chatJoinReqCtx{}); msg != nil {
  442. return msg
  443. }
  444. return []string{}
  445. }
  446. // requestChatService asks the BOS server for a connection to the chat room
  447. // identified by cookie. ChatServiceResponse finishes the join once the
  448. // connection details arrive.
  449. func (s OSCARProxy) requestChatService(ctx context.Context, me *tocConn, chatID int, roomName string, cookie string) []string {
  450. snac := wire.SNAC_0x01_0x04_OServiceServiceRequest{
  451. FoodGroup: wire.Chat,
  452. TLVRestBlock: wire.TLVRestBlock{
  453. TLVList: wire.TLVList{
  454. wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
  455. Cookie: cookie,
  456. }),
  457. },
  458. },
  459. }
  460. return s.send(ctx, me, me.bos, wire.OService, wire.OServiceServiceRequest, snac, chatServiceReqCtx{
  461. chatID: chatID,
  462. roomName: roomName,
  463. })
  464. }
  465. // connectChat opens a connection to the chat room identified by loginCookie
  466. // and signs the user into the room.
  467. func (s OSCARProxy) connectChat(ctx context.Context, me *tocConn, loginCookie []byte) (oscarConn, error) {
  468. conn, err := s.connectService(me.clientAddr, loginCookie)
  469. if err != nil {
  470. return nil, fmt.Errorf("connectService: %w", err)
  471. }
  472. subscribe := wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
  473. ClassIDs: []uint16{1, 2, 3, 4, 5},
  474. }
  475. if msg := s.send(ctx, me, conn, wire.OService, wire.OServiceRateParamsSubAdd, subscribe, nil); msg != nil {
  476. conn.Close()
  477. return nil, errors.New("unable to subscribe to rate limit changes")
  478. }
  479. if msg := s.send(ctx, me, conn, wire.OService, wire.OServiceClientOnline, wire.SNAC_0x01_0x02_OServiceClientOnline{}, nil); msg != nil {
  480. conn.Close()
  481. return nil, errors.New("unable to send client online")
  482. }
  483. return conn, nil
  484. }
  485. // ChatLeave handles the toc_chat_leave TOC command.
  486. //
  487. // From the TiK documentation:
  488. //
  489. // Leave the chat room.
  490. //
  491. // Command syntax: toc_chat_leave <Chat Room ID>
  492. func (s OSCARProxy) ChatLeave(ctx context.Context, me *tocConn, args []byte) []string {
  493. var chatIDStr string
  494. if _, err := parseArgs(args, &chatIDStr); err != nil {
  495. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  496. }
  497. chatID, err := strconv.Atoi(chatIDStr)
  498. if err != nil {
  499. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  500. }
  501. conn := me.chats.RetrieveConn(chatID)
  502. if conn == nil {
  503. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveConn: chat session `%d` not found", chatID))
  504. }
  505. conn.Close() // stop async server SNAC reply handler for this chat room
  506. me.chats.RemoveConn(chatID)
  507. return []string{fmt.Sprintf("CHAT_LEFT:%d", chatID)}
  508. }
  509. // ChatSend handles the toc_chat_send TOC command.
  510. //
  511. // From the TiK documentation:
  512. //
  513. // Send a message in a chat room using the chat room id from CHAT_JOIN. Since
  514. // reflection is always on in TOC, you do not need to add the message to your
  515. // chat UI, since you will get a CHAT_IN with the message. Remember to quote
  516. // and encode the message.
  517. //
  518. // The reflected message is sent by ChatIn.
  519. //
  520. // Command syntax: toc_chat_send <Chat Room ID> <Message>
  521. func (s OSCARProxy) ChatSend(ctx context.Context, me *tocConn, args []byte) []string {
  522. var chatIDStr, msg string
  523. if _, err := parseArgs(args, &chatIDStr, &msg); err != nil {
  524. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  525. }
  526. msg = unescape(msg)
  527. chatID, err := strconv.Atoi(chatIDStr)
  528. if err != nil {
  529. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  530. }
  531. conn := me.chats.RetrieveConn(chatID)
  532. if conn == nil {
  533. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveConn: session for chat ID `%d` not found", chatID))
  534. }
  535. block := wire.TLVRestBlock{}
  536. block.Append(wire.NewTLVBE(wire.ChatTLVEnableReflectionFlag, uint8(1)))
  537. block.Append(wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}))
  538. block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  539. TLVList: wire.TLVList{
  540. wire.NewTLVBE(wire.ChatTLVMessageInfoText, msg),
  541. },
  542. }))
  543. snac := wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  544. Channel: wire.ICBMChannelMIME,
  545. TLVRestBlock: block,
  546. }
  547. if msg := s.send(ctx, me, conn, wire.Chat, wire.ChatChannelMsgToHost, snac, nil); msg != nil {
  548. return msg
  549. }
  550. return []string{}
  551. }
  552. // ChatWhisper handles the toc_chat_whisper TOC command.
  553. //
  554. // From the TiK documentation:
  555. //
  556. // Send a message in a chat room using the chat room id from CHAT_JOIN.
  557. // This message is directed at only one person. (Currently you DO need to add
  558. // this to your UI.) Remember to quote and encode the message. Chat whispering
  559. // is different from IMs since it is linked to a chat room, and should usually
  560. // be displayed in the chat room UI.
  561. //
  562. // Command syntax: toc_chat_whisper <Chat Room ID> <dst_user> <Message>
  563. func (s OSCARProxy) ChatWhisper(ctx context.Context, me *tocConn, args []byte) []string {
  564. var chatIDStr, recip, msg string
  565. if _, err := parseArgs(args, &chatIDStr, &recip, &msg); err != nil {
  566. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  567. }
  568. msg = unescape(msg)
  569. chatID, err := strconv.Atoi(chatIDStr)
  570. if err != nil {
  571. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  572. }
  573. conn := me.chats.RetrieveConn(chatID)
  574. if conn == nil {
  575. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveConn: session for chat ID `%d` not found", chatID))
  576. }
  577. block := wire.TLVRestBlock{}
  578. block.Append(wire.NewTLVBE(wire.ChatTLVWhisperToUser, recip))
  579. block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  580. TLVList: wire.TLVList{
  581. wire.NewTLVBE(wire.ChatTLVMessageInfoText, msg),
  582. },
  583. }))
  584. snac := wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  585. Channel: wire.ICBMChannelMIME,
  586. TLVRestBlock: block,
  587. }
  588. if msg := s.send(ctx, me, conn, wire.Chat, wire.ChatChannelMsgToHost, snac, nil); msg != nil {
  589. return msg
  590. }
  591. return []string{}
  592. }
  593. // Evil handles the toc_evil TOC command.
  594. //
  595. // From the TiK documentation:
  596. //
  597. // Evil/Warn someone else. The 2nd argument is either the string "norm" for a
  598. // normal warning, or "anon" for an anonymous warning. You can only evil
  599. // people who have recently sent you ims. The higher someones evil level, the
  600. // slower they can send message.
  601. //
  602. // An ERROR message will be sent back to the client if the warning was
  603. // unsuccessful (see SNACError).
  604. //
  605. // Command syntax: toc_evil <User> <norm|anon>
  606. func (s OSCARProxy) Evil(ctx context.Context, me *tocConn, args []byte) []string {
  607. var user, scope string
  608. if _, err := parseArgs(args, &user, &scope); err != nil {
  609. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  610. }
  611. snac := wire.SNAC_0x04_0x08_ICBMEvilRequest{
  612. ScreenName: user,
  613. }
  614. switch scope {
  615. case "anon":
  616. snac.SendAs = 1
  617. case "norm":
  618. snac.SendAs = 0
  619. default:
  620. return s.runtimeErr(ctx, fmt.Errorf("incorrect warning type `%s`. allowed values: anon, norm", scope))
  621. }
  622. if msg := s.send(ctx, me, me.bos, wire.ICBM, wire.ICBMEvilRequest, snac, evilReqCtx{}); msg != nil {
  623. return msg
  624. }
  625. return []string{}
  626. }
  627. // FormatNickname handles the toc_format_nickname TOC command.
  628. //
  629. // From the TiK documentation:
  630. //
  631. // Reformat a user's nickname. An ADMIN_NICK_STATUS or ERROR message will be
  632. // sent back to the client.
  633. //
  634. // From the BizTOCSock documentation:
  635. //
  636. // [NICK is also sent with ADMIN_NICK_STATUS. This gets called [...] whenever
  637. // you do a format change.
  638. //
  639. // The reply is sent by AdminChangeReply.
  640. //
  641. // Command syntax: toc_format_nickname <new_format>
  642. func (s OSCARProxy) FormatNickname(ctx context.Context, me *tocConn, args []byte) []string {
  643. var newFormat string
  644. if _, err := parseArgs(args, &newFormat); err != nil {
  645. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  646. }
  647. // remove curly braces added by TiK
  648. newFormat = strings.Trim(newFormat, "{}")
  649. snac := wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  650. TLVRestBlock: wire.TLVRestBlock{
  651. TLVList: wire.TLVList{
  652. wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, newFormat),
  653. },
  654. },
  655. }
  656. if msg := s.send(ctx, me, me.bos, wire.Admin, wire.AdminInfoChangeRequest, snac, formatNickReqCtx{}); msg != nil {
  657. return msg
  658. }
  659. return []string{}
  660. }
  661. // GetDirSearchURL handles the toc_dir_search TOC command.
  662. //
  663. // From the TiK documentation:
  664. //
  665. // Perform a search of the Oscar Directory, using colon separated fields as in:
  666. //
  667. // "first name":"middle name":"last name":"maiden name":"city":"state":"country":"email"
  668. //
  669. // You can search by keyword by setting search terms in the 11th position (this
  670. // feature is not in the TiK docs but is present in the code):
  671. //
  672. // ::::::::::"search kw"
  673. //
  674. // Returns either a GOTO_URL or ERROR msg.
  675. //
  676. // Command syntax: toc_dir_search <info information>
  677. func (s OSCARProxy) GetDirSearchURL(ctx context.Context, me *tocConn, args []byte) []string {
  678. if !me.limiter.Allow() {
  679. return []string{rateLimitExceededErr}
  680. }
  681. var info string
  682. if _, err := parseArgs(args, &info); err != nil {
  683. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  684. }
  685. info = unescape(info)
  686. params := strings.Split(info, ":")
  687. labels := []string{
  688. "first_name",
  689. "middle_name",
  690. "last_name",
  691. "maiden_name",
  692. "city",
  693. "state",
  694. "country",
  695. "email",
  696. "nop", // unused placeholder
  697. "nop",
  698. "keyword",
  699. }
  700. // map labels to param values at their corresponding positions
  701. p := url.Values{}
  702. for i, param := range params {
  703. if i >= len(labels) {
  704. break
  705. }
  706. if param != "" {
  707. p.Add(labels[i], strings.Trim(param, "\""))
  708. }
  709. }
  710. if len(p) == 0 {
  711. return s.runtimeErr(ctx, errors.New("no search fields found"))
  712. }
  713. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  714. if err != nil {
  715. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  716. }
  717. p.Add("cookie", cookie)
  718. return []string{fmt.Sprintf("GOTO_URL:search results:dir_search?%s", p.Encode())}
  719. }
  720. // GetDirURL handles the toc_get_dir TOC command.
  721. //
  722. // From the TiK documentation:
  723. //
  724. // Gets a user's dir info a GOTO_URL or ERROR message will be sent back to the client.
  725. //
  726. // Command syntax: toc_get_dir <username>
  727. func (s OSCARProxy) GetDirURL(ctx context.Context, me *tocConn, args []byte) []string {
  728. if !me.limiter.Allow() {
  729. return []string{rateLimitExceededErr}
  730. }
  731. var user string
  732. if _, err := parseArgs(args, &user); err != nil {
  733. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  734. }
  735. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  736. if err != nil {
  737. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  738. }
  739. p := url.Values{}
  740. p.Add("cookie", cookie)
  741. p.Add("user", user)
  742. return []string{fmt.Sprintf("GOTO_URL:directory info:dir_info?%s", p.Encode())}
  743. }
  744. // GetInfoURL handles the toc_get_info TOC command.
  745. //
  746. // From the TiK documentation:
  747. //
  748. // Gets a user's info a GOTO_URL or ERROR message will be sent back to the client.
  749. //
  750. // Command syntax: toc_get_info <username>
  751. func (s OSCARProxy) GetInfoURL(ctx context.Context, me *tocConn, args []byte) []string {
  752. if !me.limiter.Allow() {
  753. return []string{rateLimitExceededErr}
  754. }
  755. var user string
  756. if _, err := parseArgs(args, &user); err != nil {
  757. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  758. }
  759. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  760. if err != nil {
  761. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  762. }
  763. p := url.Values{}
  764. p.Add("cookie", cookie)
  765. p.Add("from", me.IdentScreenName().String())
  766. p.Add("user", user)
  767. return []string{fmt.Sprintf("GOTO_URL:profile:info?%s", p.Encode())}
  768. }
  769. // GetStatus handles the toc_get_status TOC command.
  770. //
  771. // From the BlueTOC documentation:
  772. //
  773. // This useful command wasn't ever really documented. It returns either an
  774. // UPDATE_BUDDY message or an ERROR message depending on whether or not the
  775. // guy appears to be online. The command is used in both TOC1 and TOC2 with
  776. // the same syntax.
  777. //
  778. // The reply is sent by UserInfoReply or SNACError.
  779. //
  780. // Command syntax: toc_get_status <screenname>
  781. func (s OSCARProxy) GetStatus(ctx context.Context, me *tocConn, args []byte) []string {
  782. var them string
  783. if _, err := parseArgs(args, &them); err != nil {
  784. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  785. }
  786. snac := wire.SNAC_0x02_0x05_LocateUserInfoQuery{
  787. ScreenName: them,
  788. }
  789. if msg := s.send(ctx, me, me.bos, wire.Locate, wire.LocateUserInfoQuery, snac, getStatusReqCtx{screenName: them}); msg != nil {
  790. return msg
  791. }
  792. return []string{}
  793. }
  794. // InitDone handles the toc_init_done TOC command.
  795. //
  796. // From the TiK documentation:
  797. //
  798. // Tells TOC that we are ready to go online. TOC clients should first send TOC
  799. // the buddy list and any permit/deny lists. However, toc_init_done must be
  800. // called within 30 seconds after toc_signon, or the connection will be
  801. // dropped. Remember, it can't be called until after the SIGN_ON message is
  802. // received. Calling this before or multiple times after a SIGN_ON will cause
  803. // the connection to be dropped.
  804. //
  805. // Note: The business logic described in the last 3 sentences are not yet
  806. // implemented.
  807. //
  808. // Command syntax: toc_init_done
  809. func (s OSCARProxy) InitDone(ctx context.Context, me *tocConn) []string {
  810. if msg := s.send(ctx, me, me.bos, wire.OService, wire.OServiceClientOnline, wire.SNAC_0x01_0x02_OServiceClientOnline{}, nil); msg != nil {
  811. return msg
  812. }
  813. return []string{}
  814. }
  815. // RemoveBuddy handles the toc_remove_buddy TOC command.
  816. //
  817. // From the TiK documentation:
  818. //
  819. // Remove buddies from your buddy list. This does not change your saved config.
  820. //
  821. // Command syntax: toc_remove_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
  822. func (s OSCARProxy) RemoveBuddy(ctx context.Context, me *tocConn, args []byte) []string {
  823. users, err := parseArgs(args)
  824. if err != nil {
  825. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  826. }
  827. snac := wire.SNAC_0x03_0x05_BuddyDelBuddies{}
  828. for _, sn := range users {
  829. snac.Buddies = append(snac.Buddies, struct {
  830. ScreenName string `oscar:"len_prefix=uint8"`
  831. }{ScreenName: sn})
  832. }
  833. if msg := s.send(ctx, me, me.bos, wire.Buddy, wire.BuddyDelBuddies, snac, nil); msg != nil {
  834. return msg
  835. }
  836. return []string{}
  837. }
  838. // RvousAccept handles the toc_rvous_accept TOC command.
  839. //
  840. // From the TiK documentation:
  841. //
  842. // Accept a rendezvous proposal from the user <nick>. <cookie> is the cookie
  843. // from the RVOUS_PROPOSE message. <service> is the UUID the proposal was for.
  844. // <tlvlist> contains a list of tlv tags followed by base64 encoded values.
  845. //
  846. // Note: This method does not actually process the TLV list param, as it's not
  847. // passed in the TiK client, the reference implementation.
  848. //
  849. // Command syntax: toc_rvous_accept <nick> <cookie> <service>
  850. func (s OSCARProxy) RvousAccept(ctx context.Context, me *tocConn, args []byte) []string {
  851. var nick, cookie, service string
  852. if _, err := parseArgs(args, &nick, &cookie, &service); err != nil {
  853. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  854. }
  855. cbytes, err := base64.StdEncoding.DecodeString(cookie)
  856. if err != nil {
  857. return s.runtimeErr(ctx, fmt.Errorf("base64.Decode: %w", err))
  858. }
  859. var arr [8]byte
  860. copy(arr[:], cbytes) // copy slice into array
  861. svcUUID, err := uuid.Parse(service)
  862. if err != nil {
  863. return s.runtimeErr(ctx, fmt.Errorf("uuid.Parse: %w", err))
  864. }
  865. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  866. ChannelID: wire.ICBMChannelRendezvous,
  867. ScreenName: nick,
  868. TLVRestBlock: wire.TLVRestBlock{
  869. TLVList: wire.TLVList{
  870. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  871. Type: wire.ICBMRdvMessageAccept,
  872. Cookie: arr,
  873. Capability: svcUUID,
  874. }),
  875. },
  876. },
  877. }
  878. if msg := s.send(ctx, me, me.bos, wire.ICBM, wire.ICBMChannelMsgToHost, snac, nil); msg != nil {
  879. return msg
  880. }
  881. return []string{}
  882. }
  883. // RvousCancel handles the toc_rvous_cancel TOC command.
  884. //
  885. // From the TiK documentation:
  886. //
  887. // Cancel a rendezvous proposal from the user <nick>. <cookie> is the cookie
  888. // from the RVOUS_PROPOSE message. <service> is the UUID the proposal was for.
  889. // <tlvlist> contains a list of tlv tags followed by base64 encoded values.
  890. //
  891. // Note: This method does not actually process the TLV list param, as it's not
  892. // passed in the TiK client, the reference implementation.
  893. //
  894. // Command syntax: toc_rvous_cancel <nick> <cookie> <service>
  895. func (s OSCARProxy) RvousCancel(ctx context.Context, me *tocConn, args []byte) []string {
  896. var nick, cookie, service string
  897. if _, err := parseArgs(args, &nick, &cookie, &service); err != nil {
  898. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  899. }
  900. cbytes, err := base64.StdEncoding.DecodeString(cookie)
  901. if err != nil {
  902. return s.runtimeErr(ctx, fmt.Errorf("base64.Decode: %w", err))
  903. }
  904. var arr [8]byte
  905. copy(arr[:], cbytes) // copy slice into array
  906. svcUUID, err := uuid.Parse(service)
  907. if err != nil {
  908. return s.runtimeErr(ctx, fmt.Errorf("uuid.Parse: %w", err))
  909. }
  910. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  911. ChannelID: wire.ICBMChannelRendezvous,
  912. ScreenName: nick,
  913. TLVRestBlock: wire.TLVRestBlock{
  914. TLVList: wire.TLVList{
  915. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  916. Type: wire.ICBMRdvMessageCancel,
  917. Cookie: arr,
  918. Capability: svcUUID,
  919. TLVRestBlock: wire.TLVRestBlock{
  920. TLVList: wire.TLVList{
  921. wire.NewTLVBE(wire.ICBMRdvTLVTagsCancelReason, wire.ICBMRdvCancelReasonsUserCancel),
  922. },
  923. },
  924. }),
  925. },
  926. },
  927. }
  928. if msg := s.send(ctx, me, me.bos, wire.ICBM, wire.ICBMChannelMsgToHost, snac, nil); msg != nil {
  929. return msg
  930. }
  931. return []string{}
  932. }
  933. // upsertFeedbag sends feedbag items to insert or update.
  934. func (s OSCARProxy) upsertFeedbag(ctx context.Context, me *tocConn, subGroup uint16, items []wire.FeedbagItem) []string {
  935. body := any(wire.SNAC_0x13_0x08_FeedbagInsertItem{Items: items})
  936. if subGroup == wire.FeedbagUpdateItem {
  937. body = wire.SNAC_0x13_0x09_FeedbagUpdateItem{Items: items}
  938. }
  939. return s.sendFeedbagChange(ctx, me, subGroup, body, false, items)
  940. }
  941. // deleteFeedbag sends feedbag items to delete.
  942. func (s OSCARProxy) deleteFeedbag(ctx context.Context, me *tocConn, items []wire.FeedbagItem) []string {
  943. body := wire.SNAC_0x13_0x0A_FeedbagDeleteItem{Items: items}
  944. return s.sendFeedbagChange(ctx, me, wire.FeedbagDeleteItem, body, true, items)
  945. }
  946. // sendFeedbagChange sends a feedbag change and records it in the feedbag
  947. // mirror as pending until the server confirms it.
  948. func (s OSCARProxy) sendFeedbagChange(ctx context.Context, me *tocConn, subGroup uint16, body any, del bool, items []wire.FeedbagItem) []string {
  949. me.feedbag.sendMu.Lock()
  950. defer me.feedbag.sendMu.Unlock()
  951. frame := wire.SNACFrame{
  952. FoodGroup: wire.Feedbag,
  953. SubGroup: subGroup,
  954. }
  955. me.cache.add(&frame, feedbagChangeReqCtx{})
  956. me.feedbag.sent(frame.RequestID, del, items)
  957. if msg := s.sendFrame(ctx, me, me.bos, frame, body); msg != nil {
  958. me.feedbag.discard(frame.RequestID)
  959. return msg
  960. }
  961. return nil
  962. }
  963. // reloadFeedbag queries the server's feedbag to replace the feedbag mirror.
  964. // FeedbagReply finishes the reload.
  965. func (s OSCARProxy) reloadFeedbag(ctx context.Context, me *tocConn) []string {
  966. me.feedbag.sendMu.Lock()
  967. defer me.feedbag.sendMu.Unlock()
  968. return s.send(ctx, me, me.bos, wire.Feedbag, wire.FeedbagQuery, struct{}{}, feedbagReloadReqCtx{
  969. marker: me.feedbag.mark(),
  970. })
  971. }
  972. // SetPDMode handles the toc2_set_pdmode TOC2 command.
  973. //
  974. // From the BlueTOC documentation:
  975. //
  976. // Set permit/deny mode. Value: 1 = Allow all (default), 2 = Block all,
  977. // 3 = Allow "permit group" only, 4 = Block "deny group" only, 5 = Allow buddy list only.
  978. //
  979. // Command syntax: toc2_set_pdmode <mode>
  980. func (s OSCARProxy) SetPDMode(ctx context.Context, me *tocConn, args []byte) []string {
  981. var pdModeStr string
  982. if _, err := parseArgs(args, &pdModeStr); err != nil {
  983. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  984. }
  985. mode, err := strconv.Atoi(pdModeStr)
  986. if err != nil {
  987. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  988. }
  989. if mode < 1 || mode > 5 {
  990. return s.runtimeErr(ctx, errors.New("invalid pd mode specified"))
  991. }
  992. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  993. fl.SetMode(uint8(mode))
  994. if pending := fl.PendingUpdates(); len(pending) > 0 {
  995. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, pending); msg != nil {
  996. return msg
  997. }
  998. }
  999. return []string{}
  1000. }
  1001. // NewGroup handles the toc2_new_group TOC2 command.
  1002. //
  1003. // From the TOC2 docs by Jeffrey Rosen:
  1004. //
  1005. // This is an entirely new command that allows you to add groups. These should be
  1006. // quoted and you can't add more than one per command.
  1007. //
  1008. // Command syntax: toc2_new_group <group>
  1009. func (s OSCARProxy) NewGroup(ctx context.Context, me *tocConn, args []byte) []string {
  1010. var groupName string
  1011. if _, err := parseArgs(args, &groupName); err != nil {
  1012. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1013. }
  1014. groupName = unescape(groupName)
  1015. if groupName == "" {
  1016. return s.runtimeErr(ctx, fmt.Errorf("empty group name"))
  1017. }
  1018. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1019. fl.AddGroup(groupName)
  1020. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1021. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, pending); msg != nil {
  1022. return msg
  1023. }
  1024. }
  1025. return []string{}
  1026. }
  1027. // DelGroup handles the toc2_del_group TOC2 command.
  1028. //
  1029. // From the TOC2 docs by Jeffrey Rosen:
  1030. //
  1031. // Delete a group.
  1032. //
  1033. // Command syntax: toc2_del_group <group>
  1034. func (s OSCARProxy) DelGroup(ctx context.Context, me *tocConn, args []byte) []string {
  1035. var groupName string
  1036. if _, err := parseArgs(args, &groupName); err != nil {
  1037. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1038. }
  1039. groupName = unescape(groupName)
  1040. if groupName == "" {
  1041. return s.runtimeErr(ctx, fmt.Errorf("empty group name"))
  1042. }
  1043. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1044. fl.DeleteGroup(groupName)
  1045. pending := fl.PendingDeletes()
  1046. if len(pending) == 0 {
  1047. return s.runtimeErr(ctx, fmt.Errorf("group not found: %s", groupName))
  1048. }
  1049. if msg := s.deleteFeedbag(ctx, me, pending); msg != nil {
  1050. return msg
  1051. }
  1052. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1053. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, pending); msg != nil {
  1054. return msg
  1055. }
  1056. }
  1057. return []string{}
  1058. }
  1059. // NewBuddies handles the toc2_new_buddies TOC2 command.
  1060. //
  1061. // From the TOC2 docs by Jeffrey Rosen:
  1062. //
  1063. // In TOC2.0, you must add buddies in "config format". If you sent that with the
  1064. // toc2_new_buddies command, you would add the three buddies (buddytest,
  1065. // buddytest2, and buddytest3) into the group "test". Note that if the group
  1066. // doesn't already exist, it will be created.
  1067. //
  1068. // Config format: {g:group<lf>b:buddy1<lf>b:buddy2<lf>}
  1069. // Where <lf> is a linefeed character (ASCII 10).
  1070. //
  1071. // Command syntax: toc2_new_buddies <config format>
  1072. func (s OSCARProxy) NewBuddies(ctx context.Context, me *tocConn, args []byte) []string {
  1073. // Parse config string (handle quotes, unescape)
  1074. configStr := string(args)
  1075. configStr = strings.Trim(configStr, "'\" ")
  1076. configStr = unescape(configStr)
  1077. if configStr == "" {
  1078. return s.runtimeErr(ctx, fmt.Errorf("empty config"))
  1079. }
  1080. // Parse config format
  1081. groups, err := parseTOC2Config(configStr)
  1082. if err != nil {
  1083. return s.runtimeErr(ctx, fmt.Errorf("parseTOC2Config: %w", err))
  1084. }
  1085. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1086. var replies []string
  1087. // Process each group in config (deterministic order so tests and root Order are stable)
  1088. groupNames := make([]string, 0, len(groups))
  1089. for k := range groups {
  1090. groupNames = append(groupNames, k)
  1091. }
  1092. sort.Strings(groupNames)
  1093. for _, groupName := range groupNames {
  1094. buddies := groups[groupName]
  1095. fl.AddGroup(groupName)
  1096. for _, buddy := range buddies {
  1097. // Parse buddy entry (may contain alias/note: b:buddy:alias:::::note)
  1098. parts := strings.Split(buddy, ":")
  1099. buddyName := parts[0]
  1100. if buddyName == "" {
  1101. continue
  1102. }
  1103. var alias, note string
  1104. if len(parts) > 1 {
  1105. alias = parts[1]
  1106. }
  1107. if len(parts) > 6 {
  1108. note = parts[6]
  1109. }
  1110. inserted, err := fl.AddBuddy(groupName, buddyName, alias, note)
  1111. if err != nil {
  1112. return s.runtimeErr(ctx, fmt.Errorf("fl.AddBuddy: %w", err))
  1113. }
  1114. if inserted {
  1115. replies = append(replies, fmt.Sprintf("NEW_BUDDY_REPLY2:%s:added", buddyName))
  1116. }
  1117. }
  1118. }
  1119. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1120. buddyItems := make(map[uint16][]wire.FeedbagItem)
  1121. var groupOrder []uint16
  1122. for _, item := range pending {
  1123. if item.ClassID == wire.FeedbagClassIdBuddy {
  1124. if _, ok := buddyItems[item.GroupID]; !ok {
  1125. groupOrder = append(groupOrder, item.GroupID)
  1126. }
  1127. buddyItems[item.GroupID] = append(buddyItems[item.GroupID], item)
  1128. }
  1129. }
  1130. if len(buddyItems) == 0 {
  1131. return replies
  1132. }
  1133. // all groups go in one SNAC, ahead of all buddies in another
  1134. var groups []wire.FeedbagItem
  1135. for _, item := range pending {
  1136. if item.ClassID == wire.FeedbagClassIdGroup {
  1137. groups = append(groups, item)
  1138. }
  1139. }
  1140. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagUpdateItem, groups); msg != nil {
  1141. return msg
  1142. }
  1143. var buddies []wire.FeedbagItem
  1144. for _, groupID := range groupOrder {
  1145. buddies = append(buddies, buddyItems[groupID]...)
  1146. }
  1147. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, buddies); msg != nil {
  1148. return msg
  1149. }
  1150. }
  1151. return replies
  1152. }
  1153. // RemoveBuddy2 handles the toc2_remove_buddy command.
  1154. //
  1155. // From the TOC2 docs by Jeffrey Rosen:
  1156. //
  1157. // You can remove multiple names in the same group using the syntax <screenname> <screenname> <group>.
  1158. //
  1159. // Command syntax: toc2_remove_buddy <screenname> [screenname] ... [screenname] <group>
  1160. func (s OSCARProxy) RemoveBuddy2(ctx context.Context, me *tocConn, args []byte) []string {
  1161. params, err := parseArgs(args)
  1162. if err != nil {
  1163. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1164. }
  1165. if len(params) < 2 {
  1166. return s.runtimeErr(ctx, fmt.Errorf("missing params: need at least one screenname and group"))
  1167. }
  1168. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1169. groupName := params[len(params)-1]
  1170. screenNames := params[:len(params)-1]
  1171. for _, buddyName := range screenNames {
  1172. if err := fl.DeleteBuddy(groupName, buddyName); err != nil {
  1173. return s.runtimeErr(ctx, err)
  1174. }
  1175. }
  1176. // ensure to delete buddies before groups to ensure they correctly propagate
  1177. // to concurrent sessions
  1178. if pending := fl.PendingDeletes(); len(pending) > 0 {
  1179. if msg := s.deleteFeedbag(ctx, me, pending); msg != nil {
  1180. return msg
  1181. }
  1182. }
  1183. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1184. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagUpdateItem, pending); msg != nil {
  1185. return msg
  1186. }
  1187. }
  1188. return []string{}
  1189. }
  1190. // AddPermit2 handles the toc2_add_permit TOC2 command.
  1191. //
  1192. // From the TOC2 docs by Jeffrey Rosen:
  1193. //
  1194. // Add user(s) to permit list. <screenname> should be normalized and you can add
  1195. // multiple people at a time by separating the screennames with a space.
  1196. //
  1197. // Command syntax: toc2_add_permit <screenname> [screenname] ...
  1198. func (s OSCARProxy) AddPermit2(ctx context.Context, me *tocConn, args []byte) []string {
  1199. screenNames, err := parseArgs(args)
  1200. if err != nil {
  1201. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1202. }
  1203. if len(screenNames) == 0 {
  1204. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1205. }
  1206. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1207. for _, sn := range screenNames {
  1208. fl.PermitUser(sn)
  1209. }
  1210. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1211. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, pending); msg != nil {
  1212. return msg
  1213. }
  1214. }
  1215. return []string{}
  1216. }
  1217. // RemovePermit2 handles the toc2_remove_permit TOC2 command.
  1218. //
  1219. // From the TOC2 docs by Jeffrey Rosen:
  1220. //
  1221. // Remove user(s) from permit list. <screenname> should be normalized and you can
  1222. // remove multiple people at a time by separating the screennames with a space.
  1223. //
  1224. // Command syntax: toc2_remove_permit <screenname> [screenname] ...
  1225. func (s OSCARProxy) RemovePermit2(ctx context.Context, me *tocConn, args []byte) []string {
  1226. screenNames, err := parseArgs(args)
  1227. if err != nil {
  1228. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1229. }
  1230. if len(screenNames) == 0 {
  1231. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1232. }
  1233. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1234. for _, sn := range screenNames {
  1235. fl.DeletePermit(sn)
  1236. }
  1237. if pending := fl.PendingDeletes(); len(pending) > 0 {
  1238. if msg := s.deleteFeedbag(ctx, me, pending); msg != nil {
  1239. return msg
  1240. }
  1241. }
  1242. return []string{}
  1243. }
  1244. // AddDeny2 handles the toc2_add_deny TOC2 command.
  1245. //
  1246. // From the TOC2 docs by Jeffrey Rosen:
  1247. //
  1248. // Add user(s) to deny list. <screenname> should be normalized and you can add
  1249. // multiple people at a time by separating the screennames with a space.
  1250. //
  1251. // Command syntax: toc2_add_deny <screenname> [screenname] ...
  1252. func (s OSCARProxy) AddDeny2(ctx context.Context, me *tocConn, args []byte) []string {
  1253. screenNames, err := parseArgs(args)
  1254. if err != nil {
  1255. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1256. }
  1257. if len(screenNames) == 0 {
  1258. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1259. }
  1260. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1261. for _, sn := range screenNames {
  1262. fl.DenyUser(sn)
  1263. }
  1264. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1265. if msg := s.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, pending); msg != nil {
  1266. return msg
  1267. }
  1268. }
  1269. return []string{}
  1270. }
  1271. // RemoveDeny2 handles the toc2_remove_deny TOC2 command.
  1272. //
  1273. // From the TOC2 docs by Jeffrey Rosen:
  1274. //
  1275. // Remove user(s) from deny list. <screenname> should be normalized and you can
  1276. // remove multiple people at a time by separating the screennames with a space.
  1277. //
  1278. // Command syntax: toc2_remove_deny <screenname> [screenname] ...
  1279. func (s OSCARProxy) RemoveDeny2(ctx context.Context, me *tocConn, args []byte) []string {
  1280. screenNames, err := parseArgs(args)
  1281. if err != nil {
  1282. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1283. }
  1284. if len(screenNames) == 0 {
  1285. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1286. }
  1287. fl := state.NewFeedbagList(me.feedbag.snapshot(), s.RandIntn)
  1288. for _, sn := range screenNames {
  1289. fl.DeleteDeny(sn)
  1290. }
  1291. if pending := fl.PendingDeletes(); len(pending) > 0 {
  1292. if msg := s.deleteFeedbag(ctx, me, pending); msg != nil {
  1293. return msg
  1294. }
  1295. }
  1296. return []string{}
  1297. }
  1298. // SendIMEnc handles the toc2_send_im_enc TOC2 command.
  1299. //
  1300. // From the BlueTOC source code:
  1301. //
  1302. // Sends an encoded instant message to a user
  1303. // Internally, this uses the "TOC3" encoded message format
  1304. // This encoded message version supports a few more variables as well as encoding
  1305. //
  1306. // Encoding is A (ASCII) or U (UTF-8). U messages containing non-ASCII text are
  1307. // sent as Unicode; everything else is sent as ASCII.
  1308. //
  1309. // Command syntax: toc2_send_im_enc <Destination user> "F" <Encoding> <Language> <Message> [auto]
  1310. func (s OSCARProxy) SendIMEnc(ctx context.Context, me *tocConn, args []byte) []string {
  1311. var recip, unknown, enc, lang, msg string
  1312. autoReply, err := parseArgs(args, &recip, &unknown, &enc, &lang, &msg)
  1313. if err != nil {
  1314. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1315. }
  1316. fragList := wire.ICBMFragmentListASCII
  1317. if enc == "U" {
  1318. fragList = wire.ICBMFragmentList
  1319. }
  1320. return s.sendIm(ctx, me, fragList, msg, recip, autoReply)
  1321. }
  1322. // SendIM handles the toc_send_im and toc2_send_im TOC commands.
  1323. //
  1324. // From the TiK documentation (TOC1):
  1325. //
  1326. // Send a message to a remote user. Remember to quote and encode the message.
  1327. // If the optional string "auto" is the last argument, then the auto response
  1328. // flag will be turned on for the IM.
  1329. //
  1330. // TOC2 uses the same syntax as TOC1 (BlueTOC documentation).
  1331. //
  1332. // Command syntax: toc_send_im <Destination User> <Message> [auto]
  1333. // Command syntax: toc2_send_im <user> <message> [auto]
  1334. func (s OSCARProxy) SendIM(ctx context.Context, me *tocConn, args []byte) []string {
  1335. var recip, msg string
  1336. autoReply, err := parseArgs(args, &recip, &msg)
  1337. if err != nil {
  1338. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1339. }
  1340. return s.sendIm(ctx, me, wire.ICBMFragmentListASCII, msg, recip, autoReply)
  1341. }
  1342. func (s OSCARProxy) sendIm(ctx context.Context, me *tocConn, fragList func(string) ([]wire.ICBMCh1Fragment, error), msg string, recip string, autoReply []string) []string {
  1343. frags, err := fragList(unescape(msg))
  1344. if err != nil {
  1345. return s.runtimeErr(ctx, fmt.Errorf("wire.ICBMFragmentList: %w", err))
  1346. }
  1347. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  1348. ChannelID: wire.ICBMChannelIM,
  1349. ScreenName: recip,
  1350. TLVRestBlock: wire.TLVRestBlock{
  1351. TLVList: wire.TLVList{
  1352. wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags),
  1353. },
  1354. },
  1355. }
  1356. if len(autoReply) > 0 && autoReply[0] == "auto" {
  1357. snac.Append(wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}))
  1358. }
  1359. // errors such as "user is offline" are not reported to the client since
  1360. // there is no TOC error code for them
  1361. if msg := s.send(ctx, me, me.bos, wire.ICBM, wire.ICBMChannelMsgToHost, snac, nil); msg != nil {
  1362. return msg
  1363. }
  1364. return []string{}
  1365. }
  1366. // SendClientEvent handles the toc2_client_event TOC2 command.
  1367. //
  1368. // From the BlueTOC documentation:
  1369. //
  1370. // This is used to send a typing notification. Typing status: 0 for no activity,
  1371. // 1 for typing paused, 2 for currently typing.
  1372. //
  1373. // Command syntax: toc2_client_event <user> <typing status>
  1374. func (s OSCARProxy) SendClientEvent(ctx context.Context, me *tocConn, args []byte) []string {
  1375. var user, statusStr string
  1376. if _, err := parseArgs(args, &user, &statusStr); err != nil {
  1377. return s.runtimeErr(ctx, fmt.Errorf("SendClientEvent: invalid args: %w", err))
  1378. }
  1379. status, err := strconv.ParseUint(statusStr, 10, 16)
  1380. if err != nil || status > 2 {
  1381. return s.runtimeErr(ctx, fmt.Errorf("SendClientEvent: typing status must be 0, 1, or 2, got %q", statusStr))
  1382. }
  1383. snac := wire.SNAC_0x04_0x14_ICBMClientEvent{
  1384. Cookie: 0,
  1385. ChannelID: wire.ICBMChannelIM,
  1386. ScreenName: user,
  1387. Event: uint16(status),
  1388. }
  1389. if msg := s.send(ctx, me, me.bos, wire.ICBM, wire.ICBMClientEvent, snac, nil); msg != nil {
  1390. return msg
  1391. }
  1392. return []string{}
  1393. }
  1394. // SetAway handles the toc_set_away TOC command.
  1395. //
  1396. // From the TiK documentation:
  1397. //
  1398. // If the away message is present, then the unavailable status flag is set for
  1399. // the user. If the away message is not present, then the unavailable status
  1400. // flag is unset. The away message is basic HTML, remember to encode the
  1401. // information.
  1402. //
  1403. // Command syntax: toc_set_away [<away message>]
  1404. func (s OSCARProxy) SetAway(ctx context.Context, me *tocConn, args []byte) []string {
  1405. maybeMsg, err := parseArgs(args)
  1406. if err != nil {
  1407. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1408. }
  1409. var msg string
  1410. if len(maybeMsg) > 0 {
  1411. msg = unescape(maybeMsg[0])
  1412. }
  1413. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1414. TLVRestBlock: wire.TLVRestBlock{
  1415. TLVList: wire.TLVList{
  1416. wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableData, msg),
  1417. },
  1418. },
  1419. }
  1420. if msg := s.send(ctx, me, me.bos, wire.Locate, wire.LocateSetInfo, snac, nil); msg != nil {
  1421. return msg
  1422. }
  1423. return []string{}
  1424. }
  1425. // SetCaps handles the toc_set_caps TOC command.
  1426. //
  1427. // From the TiK documentation:
  1428. //
  1429. // Set my capabilities. All capabilities that we support need to be sent at
  1430. // the same time. Capabilities are represented by UUIDs.
  1431. //
  1432. // This method automatically adds the "chat" capability since it doesn't seem
  1433. // to be sent explicitly by the official clients, even though they support
  1434. // chat.
  1435. //
  1436. // Command syntax: toc_set_caps [ <Capability 1> [<Capability 2> [...]]]
  1437. func (s OSCARProxy) SetCaps(ctx context.Context, me *tocConn, args []byte) []string {
  1438. params, err := parseArgs(args)
  1439. if err != nil {
  1440. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1441. }
  1442. // Clients may send capabilities as space-separated UUIDs or as a single
  1443. // comma-separated string (e.g. TameClone: "UUID,1348,134B,..."). Split each
  1444. // param on comma. Accept full UUIDs and OSCAR short caps (1–4 hex digits
  1445. // expanded to 0946XXYY-4C7F-11D1-8222-444553540000); ignore unknown tokens.
  1446. caps := make([]uuid.UUID, 0, 16)
  1447. for _, param := range params {
  1448. for _, token := range strings.Split(param, ",") {
  1449. token = strings.TrimSpace(token)
  1450. if token == "" {
  1451. continue
  1452. }
  1453. uid, err := uuid.Parse(token)
  1454. if err != nil {
  1455. uid, ok := wire.ShortCapHexToUUID(token)
  1456. if !ok {
  1457. continue
  1458. }
  1459. caps = append(caps, uid)
  1460. continue
  1461. }
  1462. caps = append(caps, uid)
  1463. }
  1464. }
  1465. // assume client supports chat, although we may want to do this according
  1466. // to client ID
  1467. caps = append(caps, wire.CapChat)
  1468. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1469. TLVRestBlock: wire.TLVRestBlock{
  1470. TLVList: wire.TLVList{
  1471. wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, caps),
  1472. },
  1473. },
  1474. }
  1475. if msg := s.send(ctx, me, me.bos, wire.Locate, wire.LocateSetInfo, snac, nil); msg != nil {
  1476. return msg
  1477. }
  1478. return []string{}
  1479. }
  1480. // SetConfig handles the toc_set_config TOC command.
  1481. //
  1482. // From the TiK documentation:
  1483. //
  1484. // Set the config information for this user. The config information is line
  1485. // oriented with the first character being the item type, followed by a space,
  1486. // with the rest of the line being the item value. Only letters, numbers, and
  1487. // spaces should be used. Remember you will have to enclose the entire config
  1488. // in quotes.
  1489. //
  1490. // Item Types:
  1491. // - g - Buddy Group (All Buddies until the next g or the end of config are in this group.)
  1492. // - b - A Buddy
  1493. // - p - Person on permit list
  1494. // - d - Person on deny list
  1495. // - m - Permit/Deny Mode. Possible values are
  1496. // - 1 - Permit All
  1497. // - 2 - Deny All
  1498. // - 3 - Permit Some
  1499. // - 4 - Deny Some
  1500. //
  1501. // This method doesn't attempt to validate any of the configuration--it saves
  1502. // the config as received from the client.
  1503. //
  1504. // Command syntax: toc_set_config <Config Info>
  1505. func (s OSCARProxy) SetConfig(ctx context.Context, me *tocConn, args []byte) []string {
  1506. if !me.limiter.Allow() {
  1507. return []string{rateLimitExceededErr}
  1508. }
  1509. // most TOC clients don't quote the config info argument, despite what the
  1510. // documentation specifies. this makes the argument payload incompatible
  1511. // for CSV parsing. since this command takes a single argument, we can get
  1512. // away with trimming quotes and spaces from the byte slice before passing
  1513. // it to the config store.
  1514. args = bytes.Trim(args, "'\" ")
  1515. cfg := string(args)
  1516. if cfg == "" {
  1517. return s.runtimeErr(ctx, fmt.Errorf("empty config"))
  1518. }
  1519. if err := s.TOCConfigStore.SetTOCConfig(ctx, me.IdentScreenName(), cfg); err != nil {
  1520. return s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.SaveTOCConfig: %w", err))
  1521. }
  1522. return []string{}
  1523. }
  1524. // SetDir handles the toc_set_dir TOC command.
  1525. //
  1526. // From the TiK documentation:
  1527. //
  1528. // Set the DIR user information. This is a colon separated fields as in:
  1529. //
  1530. // "first name":"middle name":"last name":"maiden name":"city":"state":"country":"email":"allow web searches".
  1531. //
  1532. // Should return a DIR_STATUS msg. Having anything in the "allow web searches"
  1533. // field allows people to use web-searches to find your directory info.
  1534. // Otherwise, they'd have to use the client.
  1535. //
  1536. // The fields "email" and "allow web searches" are ignored by this method.
  1537. //
  1538. // Command syntax: toc_set_dir <info information>
  1539. func (s OSCARProxy) SetDir(ctx context.Context, me *tocConn, args []byte) []string {
  1540. var info string
  1541. if _, err := parseArgs(args, &info); err != nil {
  1542. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1543. }
  1544. info = unescape(info)
  1545. rawFields := strings.Split(info, ":")
  1546. var finalFields [9]string
  1547. if len(rawFields) > len(finalFields) {
  1548. return s.runtimeErr(ctx, fmt.Errorf("expected at most %d params, got %d", len(finalFields), len(rawFields)))
  1549. }
  1550. for i, a := range rawFields {
  1551. finalFields[i] = strings.Trim(a, "\"")
  1552. }
  1553. snac := wire.SNAC_0x02_0x09_LocateSetDirInfo{
  1554. TLVRestBlock: wire.TLVRestBlock{
  1555. TLVList: wire.TLVList{
  1556. wire.NewTLVBE(wire.ODirTLVFirstName, finalFields[0]),
  1557. wire.NewTLVBE(wire.ODirTLVMiddleName, finalFields[1]),
  1558. wire.NewTLVBE(wire.ODirTLVLastName, finalFields[2]),
  1559. wire.NewTLVBE(wire.ODirTLVMaidenName, finalFields[3]),
  1560. wire.NewTLVBE(wire.ODirTLVCountry, finalFields[6]),
  1561. wire.NewTLVBE(wire.ODirTLVState, finalFields[5]),
  1562. wire.NewTLVBE(wire.ODirTLVCity, finalFields[4]),
  1563. },
  1564. },
  1565. }
  1566. if msg := s.send(ctx, me, me.bos, wire.Locate, wire.LocateSetDirInfo, snac, nil); msg != nil {
  1567. return msg
  1568. }
  1569. return []string{}
  1570. }
  1571. // SetIdle handles the toc_set_idle TOC command.
  1572. //
  1573. // From the TiK documentation:
  1574. //
  1575. // Set idle information. If <idle secs> is 0 then the user isn't idle at all.
  1576. // If <idle secs> is greater than 0 then the user has already been idle for
  1577. // <idle secs> number of seconds. The server will automatically keep
  1578. // incrementing this number, so do not repeatedly call with new idle times.
  1579. //
  1580. // Command syntax: toc_set_idle <idle secs>
  1581. func (s OSCARProxy) SetIdle(ctx context.Context, me *tocConn, args []byte) []string {
  1582. var idleTimeStr string
  1583. if _, err := parseArgs(args, &idleTimeStr); err != nil {
  1584. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1585. }
  1586. t, err := strconv.Atoi(idleTimeStr)
  1587. if err != nil {
  1588. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  1589. }
  1590. snac := wire.SNAC_0x01_0x11_OServiceIdleNotification{
  1591. IdleTime: uint32(t),
  1592. }
  1593. if msg := s.send(ctx, me, me.bos, wire.OService, wire.OServiceIdleNotification, snac, nil); msg != nil {
  1594. return msg
  1595. }
  1596. return []string{}
  1597. }
  1598. // SetInfo handles the toc_set_info TOC command.
  1599. //
  1600. // From the TiK documentation:
  1601. //
  1602. // Set the LOCATE user information. This is basic HTML. Remember to encode the info.
  1603. //
  1604. // Command syntax: toc_set_info <info information>
  1605. func (s OSCARProxy) SetInfo(ctx context.Context, me *tocConn, args []byte) []string {
  1606. var info string
  1607. if _, err := parseArgs(args, &info); err != nil {
  1608. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1609. }
  1610. info = unescape(info)
  1611. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1612. TLVRestBlock: wire.TLVRestBlock{
  1613. TLVList: wire.TLVList{
  1614. wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, info),
  1615. },
  1616. },
  1617. }
  1618. if msg := s.send(ctx, me, me.bos, wire.Locate, wire.LocateSetInfo, snac, nil); msg != nil {
  1619. return msg
  1620. }
  1621. return []string{}
  1622. }
  1623. // Signon handles the toc_signon, toc2_signon, and toc2_login TOC commands.
  1624. //
  1625. // From the TiK documentation (TOC1 toc_signon):
  1626. //
  1627. // The password needs to be roasted with the Roasting String if coming over a
  1628. // FLAP connection, CP connections don't use roasted passwords. The language
  1629. // specified will be used when generating web pages, such as the get info
  1630. // pages. Currently, the only supported language is "english". If the language
  1631. // sent isn't found, the default "english" language will be used. The version
  1632. // string will be used for the client identity, and must be less than 50
  1633. // characters.
  1634. //
  1635. // Passwords are roasted when sent to the host. This is done so they aren't
  1636. // sent in "clear text" over the wire, although they are still trivial to
  1637. // decode. Roasting is performed by first xoring each byte in the password
  1638. // with the equivalent modulo byte in the roasting string. The result is then
  1639. // converted to ascii hex, and prepended with "0x". So for example the
  1640. // password "password" roasts to "0x2408105c23001130".
  1641. //
  1642. // The Roasting String is Tic/Toc.
  1643. //
  1644. // toc2_signon uses the same parameters as toc_signon. toc2_login (TOC2) may
  1645. // include additional trailing parameters (e.g. agent string, code); see
  1646. // BlueTOC documentation.
  1647. //
  1648. // It returns the user's TOC connection, or nil if signon failed.
  1649. //
  1650. // Command syntax: toc_signon <authorizer host> <authorizer port> <User Name> <Password> <language> <version>
  1651. // Command syntax: toc2_signon <authorizer host> <authorizer port> <User Name> <Password> <language> <version>
  1652. // Command syntax: toc2_login <authorizer host> <authorizer port> <User Name> <Password> <language> <version> [additional params...]
  1653. func (s OSCARProxy) Signon(ctx context.Context, args []byte, clientAddr net.Addr) (*tocConn, []string) {
  1654. var cmd, userName, password string
  1655. if _, err := parseArgs(args, &cmd, nil, nil, &userName, &password); err != nil {
  1656. return nil, s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1657. }
  1658. switch cmd {
  1659. case "toc_signon", "toc2_signon", "toc2_login":
  1660. // valid
  1661. default:
  1662. return nil, s.runtimeErr(ctx, errors.New("expected one of toc_signon, toc2_signon, toc2_login"))
  1663. }
  1664. if len(password) < 3 {
  1665. return nil, s.runtimeErr(ctx, fmt.Errorf("password too short for roasted hex (need 2-char prefix + hex)"))
  1666. }
  1667. passwordHash, err := hex.DecodeString(password[2:])
  1668. if err != nil {
  1669. return nil, s.runtimeErr(ctx, fmt.Errorf("hex.DecodeString: %w", err))
  1670. }
  1671. signonTLVs := []wire.TLV{
  1672. wire.NewTLVBE(wire.LoginTLVTagsScreenName, userName),
  1673. wire.NewTLVBE(wire.LoginTLVTagsRoastedTOCPassword, passwordHash),
  1674. }
  1675. if cmd == "toc2_login" || cmd == "toc2_signon" {
  1676. signonTLVs = append(signonTLVs, wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient))
  1677. }
  1678. block, err := s.authenticate(clientAddr, signonTLVs)
  1679. if err != nil {
  1680. return nil, s.runtimeErr(ctx, fmt.Errorf("authenticate: %w", err))
  1681. }
  1682. if code, failed := block.Uint16BE(wire.LoginTLVTagsErrorSubcode); failed {
  1683. s.Logger.DebugContext(ctx, "login failed", "code", code)
  1684. if code == wire.LoginErrRateLimitExceeded {
  1685. return nil, []string{"ERROR:" + wire.TOCErrorAuthRatedFromLogin}
  1686. }
  1687. return nil, []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword}
  1688. }
  1689. authCookie, ok := block.Bytes(wire.OServiceTLVTagsLoginCookie)
  1690. if !ok {
  1691. return nil, s.runtimeErr(ctx, fmt.Errorf("unable to get session id from payload"))
  1692. }
  1693. bos, err := s.connectService(clientAddr, authCookie)
  1694. if err != nil {
  1695. return nil, s.runtimeErr(ctx, fmt.Errorf("connectService: %w", err))
  1696. }
  1697. me := newTOCConn(bos, state.NewIdentScreenName(userName), clientAddr)
  1698. me.isTOC2 = cmd != "toc_signon"
  1699. me.supportsMsgEnc = cmd == "toc2_login"
  1700. msgs, err := s.signonConfig(ctx, me, cmd)
  1701. if err != nil {
  1702. bos.Close()
  1703. return nil, s.runtimeErr(ctx, err)
  1704. }
  1705. return me, msgs
  1706. }
  1707. // signonConfig prepares a newly signed-on TOC connection and returns the
  1708. // SIGN_ON, NICK and config messages for the client.
  1709. func (s OSCARProxy) signonConfig(ctx context.Context, me *tocConn, cmd string) ([]string, error) {
  1710. // learn when the server starts dropping the user's requests
  1711. subscribe := wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
  1712. ClassIDs: []uint16{1, 2, 3, 4, 5},
  1713. }
  1714. if msg := s.send(ctx, me, me.bos, wire.OService, wire.OServiceRateParamsSubAdd, subscribe, nil); msg != nil {
  1715. return nil, errors.New("unable to subscribe to rate limit changes")
  1716. }
  1717. // advertise chat support, since TOC clients don't report capabilities
  1718. // until they send toc_set_caps
  1719. caps := wire.SNAC_0x02_0x04_LocateSetInfo{
  1720. TLVRestBlock: wire.TLVRestBlock{
  1721. TLVList: wire.TLVList{
  1722. wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, []uuid.UUID{wire.CapChat}),
  1723. },
  1724. },
  1725. }
  1726. if msg := s.send(ctx, me, me.bos, wire.Locate, wire.LocateSetInfo, caps, nil); msg != nil {
  1727. return nil, errors.New("unable to set capabilities")
  1728. }
  1729. reply, err := s.await(ctx, me, wire.OService, wire.OServiceUserInfoQuery, struct{}{})
  1730. if err != nil {
  1731. return nil, fmt.Errorf("unable to query user info: %w", err)
  1732. }
  1733. userInfo, ok := reply.Body.(wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate)
  1734. if !ok || len(userInfo.UserInfo) == 0 {
  1735. return nil, fmt.Errorf("unexpected user info reply %v", reply.Frame)
  1736. }
  1737. nick := "NICK:" + userInfo.UserInfo[0].ScreenName
  1738. if cmd == "toc_signon" {
  1739. u, err := s.TOCConfigStore.User(ctx, me.IdentScreenName())
  1740. if err != nil {
  1741. return nil, fmt.Errorf("TOCConfigStore.User: %w", err)
  1742. }
  1743. if u == nil {
  1744. return nil, fmt.Errorf("TOCConfigStore.User: user not found")
  1745. }
  1746. return []string{"SIGN_ON:TOC1.0", fmt.Sprintf("CONFIG:%s", u.TOCConfig), nick}, nil
  1747. }
  1748. if msg := s.send(ctx, me, me.bos, wire.Feedbag, wire.FeedbagUse, struct{}{}, nil); msg != nil {
  1749. return nil, errors.New("unable to use feedbag")
  1750. }
  1751. reply, err = s.await(ctx, me, wire.Feedbag, wire.FeedbagQuery, struct{}{})
  1752. if err != nil {
  1753. return nil, fmt.Errorf("unable to query feedbag: %w", err)
  1754. }
  1755. feedbag, ok := reply.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  1756. if !ok {
  1757. return nil, fmt.Errorf("unexpected feedbag reply %v", reply.Frame)
  1758. }
  1759. me.feedbag.reset(feedbag.Items)
  1760. cfg, err := buildToc2Config(feedbag.Items)
  1761. if err != nil {
  1762. return nil, fmt.Errorf("buildToc2Config: %w", err)
  1763. }
  1764. return append([]string{"SIGN_ON:TOC2.0", nick}, cfg...), nil
  1765. }
  1766. // await sends a SNAC request over the BOS connection and waits for its reply.
  1767. // SNACs that arrive in the meantime are kept for RecvBOS. It is only used
  1768. // during signon, before RecvBOS starts.
  1769. func (s OSCARProxy) await(ctx context.Context, me *tocConn, foodGroup uint16, subGroup uint16, body any) (wire.SNACMessage, error) {
  1770. frame := wire.SNACFrame{
  1771. FoodGroup: foodGroup,
  1772. SubGroup: subGroup,
  1773. }
  1774. me.cache.add(&frame, nil)
  1775. defer me.cache.remove(frame.RequestID)
  1776. if err := me.bos.SendSNAC(frame, body); err != nil {
  1777. return wire.SNACMessage{}, fmt.Errorf("SendSNAC: %w", err)
  1778. }
  1779. timeout := time.NewTimer(signonTimeout)
  1780. defer timeout.Stop()
  1781. for {
  1782. select {
  1783. case msg := <-me.bos.Messages():
  1784. if msg.Frame.FoodGroup != foodGroup || msg.Frame.RequestID != frame.RequestID {
  1785. me.backlog = append(me.backlog, msg)
  1786. continue
  1787. }
  1788. if snacErr, ok := msg.Body.(wire.SNACError); ok {
  1789. return wire.SNACMessage{}, fmt.Errorf("received SNAC error code %d", snacErr.Code)
  1790. }
  1791. return msg, nil
  1792. case <-me.bos.Closed():
  1793. return wire.SNACMessage{}, errSignedOff
  1794. case <-timeout.C:
  1795. return wire.SNACMessage{}, errors.New("timed out waiting for reply")
  1796. case <-ctx.Done():
  1797. return wire.SNACMessage{}, ctx.Err()
  1798. }
  1799. }
  1800. }
  1801. // buildToc2Config constructs configuration for CONFIG2
  1802. // - Lines are separated by LF; data between colons.
  1803. // - g: = group (before buddies in that group), b: = buddy (optionally :alias and :::::note),
  1804. // - d: = blocked, p: = private (permit), m: = privacy level (1-5), done: = end.
  1805. func buildToc2Config(fb []wire.FeedbagItem) ([]string, error) {
  1806. type buddy struct {
  1807. name string
  1808. alias string
  1809. note string
  1810. }
  1811. type group struct {
  1812. name string
  1813. buddies map[uint16]buddy
  1814. order []uint16
  1815. }
  1816. type feedbag struct {
  1817. order []uint16
  1818. groups map[uint16]group
  1819. }
  1820. buddylist := feedbag{
  1821. groups: make(map[uint16]group),
  1822. }
  1823. for _, item := range fb {
  1824. if item.ClassID == wire.FeedbagClassIdGroup {
  1825. // root group contains the order of all other groups
  1826. if item.GroupID == 0 {
  1827. val, hasVal := item.Uint16SliceBE(wire.FeedbagAttributesOrder)
  1828. if !hasVal {
  1829. return []string{}, fmt.Errorf("root group missing order attribute")
  1830. }
  1831. buddylist.order = val
  1832. continue
  1833. }
  1834. group := group{
  1835. name: item.Name,
  1836. buddies: make(map[uint16]buddy),
  1837. }
  1838. // order will be empty if the group is empty
  1839. val, _ := item.Uint16SliceBE(wire.FeedbagAttributesOrder)
  1840. group.order = val
  1841. buddylist.groups[item.GroupID] = group
  1842. }
  1843. }
  1844. var cfg []string
  1845. for _, item := range fb {
  1846. if item.ClassID == wire.FeedbagClassIdBuddy {
  1847. // Ensure group exists (handle orphaned buddies)
  1848. if _, exists := buddylist.groups[item.GroupID]; !exists {
  1849. buddylist.groups[item.GroupID] = group{
  1850. name: "", // Unknown group name
  1851. buddies: make(map[uint16]buddy),
  1852. order: []uint16{},
  1853. }
  1854. }
  1855. buddy := buddy{
  1856. name: item.Name,
  1857. }
  1858. if val, hasVal := item.String(wire.FeedbagAttributesNote); hasVal {
  1859. buddy.note = val
  1860. }
  1861. if val, hasVal := item.String(wire.FeedbagAttributesAlias); hasVal {
  1862. buddy.alias = val
  1863. }
  1864. buddylist.groups[item.GroupID].buddies[item.ItemID] = buddy
  1865. }
  1866. if item.ClassID == wire.FeedbagClassIDDeny {
  1867. cfg = append(cfg, "d:"+item.Name)
  1868. }
  1869. if item.ClassID == wire.FeedbagClassIDPermit {
  1870. cfg = append(cfg, "p:"+item.Name)
  1871. }
  1872. if item.ClassID == wire.FeedbagClassIdPdinfo {
  1873. val, hasVal := item.Uint8(wire.FeedbagAttributesPdMode)
  1874. if hasVal {
  1875. cfg = append(cfg, fmt.Sprintf("m:%d", val))
  1876. }
  1877. }
  1878. }
  1879. for _, gid := range buddylist.order {
  1880. group, ok := buddylist.groups[gid]
  1881. if !ok {
  1882. continue // the root group lists a group that isn't in the feedbag
  1883. }
  1884. cfg = append(cfg, "g:"+group.name)
  1885. for _, bid := range group.order {
  1886. buddy, ok := group.buddies[bid]
  1887. if !ok {
  1888. continue // the group lists a buddy that isn't in the feedbag
  1889. }
  1890. tmpLine := "b:" + buddy.name
  1891. if buddy.alias != "" {
  1892. tmpLine += ":" + buddy.alias
  1893. }
  1894. if buddy.note != "" {
  1895. tmpLine += ":::::" + buddy.note
  1896. }
  1897. cfg = append(cfg, tmpLine)
  1898. }
  1899. }
  1900. cfg = append(cfg, "done:")
  1901. return []string{"CONFIG2:" + strings.Join(cfg, "\n") + "\n"}, nil
  1902. }
  1903. // newHTTPAuthToken creates a HMAC token for authenticating TOC HTTP requests
  1904. func (s OSCARProxy) newHTTPAuthToken(me state.IdentScreenName) (string, error) {
  1905. cookie, err := s.CookieBaker.Issue([]byte(me.String()), state.DefaultCookieTTL)
  1906. if err != nil {
  1907. return "", err
  1908. }
  1909. // trim padding so that gaim doesn't choke on the long value
  1910. cookie = bytes.TrimRight(cookie, "\x00")
  1911. return hex.EncodeToString(cookie), nil
  1912. }
  1913. // parseArgs extracts arguments from a TOC command. Each positional argument is
  1914. // assigned to its corresponding args pointer. It returns the remaining
  1915. // arguments as varargs.
  1916. func parseArgs(payload []byte, args ...*string) (varArgs []string, err error) {
  1917. if len(payload) == 0 && len(args) == 0 {
  1918. return []string{}, nil
  1919. }
  1920. reader := csv.NewReader(bytes.NewReader(payload))
  1921. reader.Comma = ' '
  1922. reader.LazyQuotes = true
  1923. reader.TrimLeadingSpace = true
  1924. segs, err := reader.Read()
  1925. if err != nil {
  1926. return []string{}, fmt.Errorf("CSV reader error: %w", err)
  1927. }
  1928. if len(segs) < len(args) {
  1929. return []string{}, fmt.Errorf("command contains fewer arguments than expected")
  1930. }
  1931. // populate placeholder pointers with their corresponding values
  1932. for i, arg := range args {
  1933. if arg != nil {
  1934. *arg = strings.TrimSpace(segs[i])
  1935. }
  1936. }
  1937. // dump remaining arguments as varargs
  1938. return segs[len(args):], err
  1939. }
  1940. // runtimeErr is a convenience function that logs an error and returns a TOC
  1941. // internal server error.
  1942. func (s OSCARProxy) runtimeErr(ctx context.Context, err error) []string {
  1943. s.Logger.ErrorContext(ctx, "internal service error", "err", err.Error())
  1944. return []string{cmdInternalSvcErr}
  1945. }
  1946. // unescape removes escaping from the following TOC characters: \ { } ( ) [ ] $ "
  1947. func unescape(encoded string) string {
  1948. if !strings.ContainsRune(encoded, '\\') {
  1949. return encoded
  1950. }
  1951. var result strings.Builder
  1952. result.Grow(len(encoded))
  1953. escaped := false
  1954. for i := 0; i < len(encoded); i++ {
  1955. ch := encoded[i]
  1956. if escaped {
  1957. // append escaped character without the backslash
  1958. result.WriteByte(ch)
  1959. escaped = false
  1960. } else if ch == '\\' {
  1961. escaped = true
  1962. } else {
  1963. result.WriteByte(ch)
  1964. }
  1965. }
  1966. return result.String()
  1967. }
  1968. // parseTOC2Config parses the TOC2 config format string into a map of group names to buddy lists.
  1969. //
  1970. // Config format: {g:group<lf>b:buddy1<lf>b:buddy2<lf>}
  1971. // Where <lf> is a linefeed character (ASCII 10, \n).
  1972. //
  1973. // Extended format for buddies with alias/note:
  1974. // b:buddy:alias:::::note
  1975. //
  1976. // Returns a map where keys are group names and values are slices of buddy entries.
  1977. // Each buddy entry is a string that may contain alias/note information.
  1978. func parseTOC2Config(config string) (map[string][]string, error) {
  1979. // Remove surrounding braces
  1980. config = strings.TrimPrefix(config, "{")
  1981. config = strings.TrimSuffix(config, "}")
  1982. // Split by linefeed (ASCII 10)
  1983. lines := strings.Split(config, "\n")
  1984. result := make(map[string][]string)
  1985. var currentGroup string
  1986. for _, line := range lines {
  1987. line = strings.TrimSpace(line)
  1988. if line == "" {
  1989. continue
  1990. }
  1991. if strings.HasPrefix(line, "g:") {
  1992. // Group line: g:groupname
  1993. currentGroup = strings.TrimPrefix(line, "g:")
  1994. if currentGroup == "" {
  1995. return nil, fmt.Errorf("empty group name")
  1996. }
  1997. if result[currentGroup] == nil {
  1998. result[currentGroup] = []string{}
  1999. }
  2000. } else if strings.HasPrefix(line, "b:") {
  2001. // Buddy line: b:buddy or b:buddy:alias:::::note
  2002. if currentGroup == "" {
  2003. return nil, fmt.Errorf("buddy entry without group")
  2004. }
  2005. buddyEntry := strings.TrimPrefix(line, "b:")
  2006. if buddyEntry == "" {
  2007. return nil, fmt.Errorf("empty buddy name")
  2008. }
  2009. result[currentGroup] = append(result[currentGroup], buddyEntry)
  2010. }
  2011. }
  2012. return result, nil
  2013. }