helpers_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. package toc
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "io"
  7. "net"
  8. "sync"
  9. "testing"
  10. "time"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/mock"
  13. "github.com/mk6i/open-oscar-server/config"
  14. "github.com/mk6i/open-oscar-server/state"
  15. "github.com/mk6i/open-oscar-server/wire"
  16. )
  17. type userInfoQueryParams []struct {
  18. me state.IdentScreenName
  19. inBody wire.SNAC_0x02_0x05_LocateUserInfoQuery
  20. msg wire.SNACMessage
  21. err error
  22. }
  23. type dirInfoParams []struct {
  24. body wire.SNAC_0x02_0x0B_LocateGetDirInfo
  25. msg wire.SNACMessage
  26. err error
  27. }
  28. type locateParams struct {
  29. userInfoQueryParams
  30. dirInfoParams
  31. }
  32. type infoQueryParams []struct {
  33. inBody wire.SNAC_0x0F_0x02_InfoQuery
  34. msg wire.SNACMessage
  35. err error
  36. }
  37. type dirSearchParams struct {
  38. infoQueryParams
  39. }
  40. type setTOCConfigParams []struct {
  41. user state.IdentScreenName
  42. config string
  43. err error
  44. }
  45. type retrieveSessionParams []struct {
  46. screenName state.IdentScreenName
  47. returnedSession *state.Session
  48. }
  49. type sessionRetrieverParams struct {
  50. retrieveSessionParams
  51. }
  52. type tocConfigParams struct {
  53. setTOCConfigParams
  54. }
  55. type feedBagParams struct {
  56. feedbagParams
  57. upsertFeedbagParams
  58. deleteFeedbagParams
  59. }
  60. // feedbagParams holds the user's feedbag before a command runs.
  61. type feedbagParams []struct {
  62. results []wire.FeedbagItem
  63. }
  64. // upsertFeedbagParams is the list of feedbag insert or update SNACs a command
  65. // is expected to send, in order. frame holds the expected subgroup, items the
  66. // exact feedbag items, and err the error the connection returns for the send.
  67. type upsertFeedbagParams []struct {
  68. frame wire.SNACFrame
  69. items []wire.FeedbagItem
  70. err error
  71. }
  72. // deleteFeedbagParams is the list of feedbag delete SNACs a command is
  73. // expected to send, in order, before any upserts.
  74. type deleteFeedbagParams []struct {
  75. frame wire.SNACFrame
  76. inBody wire.SNAC_0x13_0x0A_FeedbagDeleteItem
  77. err error
  78. }
  79. type mockParams struct {
  80. cookieBakerParams
  81. dirSearchParams
  82. locateParams
  83. feedBagParams
  84. sessionRetrieverParams
  85. tocConfigParams
  86. }
  87. // issueParams holds multiple scenarios for the Issue method.
  88. type issueParams []struct {
  89. data []byte
  90. returnData []byte
  91. returnErr error
  92. }
  93. // cookieBakerParams groups the method scenarios for a CookieBaker.
  94. type cookieBakerParams struct {
  95. issueParams issueParams
  96. }
  97. // fakeOSCARConn is an oscarConn that records the SNACs sent to it. The nth
  98. // send returns sendErrs[n], if set. Tests feed server SNACs through msgs.
  99. type fakeOSCARConn struct {
  100. mu sync.Mutex
  101. sent []wire.SNACMessage
  102. sendErrs []error
  103. msgs chan wire.SNACMessage
  104. closed chan struct{}
  105. closeOnce sync.Once
  106. }
  107. func newFakeOSCARConn() *fakeOSCARConn {
  108. return &fakeOSCARConn{
  109. msgs: make(chan wire.SNACMessage),
  110. closed: make(chan struct{}),
  111. }
  112. }
  113. func (c *fakeOSCARConn) SendSNAC(frame wire.SNACFrame, body any) error {
  114. c.mu.Lock()
  115. defer c.mu.Unlock()
  116. n := len(c.sent)
  117. c.sent = append(c.sent, wire.SNACMessage{Frame: frame, Body: body})
  118. if n < len(c.sendErrs) {
  119. return c.sendErrs[n]
  120. }
  121. return nil
  122. }
  123. func (c *fakeOSCARConn) Messages() <-chan wire.SNACMessage {
  124. return c.msgs
  125. }
  126. func (c *fakeOSCARConn) Closed() <-chan struct{} {
  127. return c.closed
  128. }
  129. func (c *fakeOSCARConn) Close() {
  130. c.closeOnce.Do(func() {
  131. close(c.closed)
  132. })
  133. }
  134. func (c *fakeOSCARConn) isClosed() bool {
  135. select {
  136. case <-c.closed:
  137. return true
  138. default:
  139. return false
  140. }
  141. }
  142. // sentSNACs returns a copy of the SNACs sent so far.
  143. func (c *fakeOSCARConn) sentSNACs() []wire.SNACMessage {
  144. c.mu.Lock()
  145. defer c.mu.Unlock()
  146. return append([]wire.SNACMessage(nil), c.sent...)
  147. }
  148. // newTestTOCConn creates a TOC connection for the user of instance whose BOS
  149. // connection is a fakeOSCARConn.
  150. func newTestTOCConn(instance *state.SessionInstance) (*tocConn, *fakeOSCARConn) {
  151. bos := newFakeOSCARConn()
  152. me := newTOCConn(bos, instance.IdentScreenName(), &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234})
  153. me.toCh = make(chan []string, 16)
  154. me.goAsync = func(f func() error) { go f() }
  155. return me, bos
  156. }
  157. // newSNAC returns the SNAC message a TOC connection sends as its reqIDth
  158. // request.
  159. func newSNAC(foodGroup uint16, subGroup uint16, reqID uint32, body any) wire.SNACMessage {
  160. return wire.SNACMessage{
  161. Frame: wire.SNACFrame{
  162. FoodGroup: foodGroup,
  163. SubGroup: subGroup,
  164. RequestID: reqID,
  165. },
  166. Body: body,
  167. }
  168. }
  169. // feedbagSNACs returns the feedbag SNACs that params expect a command to send.
  170. // A command sends at most one SNAC per subgroup, in the order deletes,
  171. // updates, inserts, so params' items are combined by subgroup in table order.
  172. // It also returns the error for each send. Sends stop at the first error.
  173. func feedbagSNACs(params feedBagParams) ([]wire.SNACMessage, []error) {
  174. var deletes, updates, inserts []wire.FeedbagItem
  175. var deleteErr, updateErr, insertErr error
  176. for _, p := range params.deleteFeedbagParams {
  177. deletes = append(deletes, p.inBody.Items...)
  178. deleteErr = errors.Join(deleteErr, p.err)
  179. }
  180. for _, p := range params.upsertFeedbagParams {
  181. if p.frame.SubGroup == wire.FeedbagUpdateItem {
  182. updates = append(updates, p.items...)
  183. updateErr = errors.Join(updateErr, p.err)
  184. } else {
  185. inserts = append(inserts, p.items...)
  186. insertErr = errors.Join(insertErr, p.err)
  187. }
  188. }
  189. var want []wire.SNACMessage
  190. var errs []error
  191. add := func(subGroup uint16, items []wire.FeedbagItem, body any, err error) bool {
  192. if len(items) == 0 {
  193. return true
  194. }
  195. want = append(want, newSNAC(wire.Feedbag, subGroup, uint32(len(want)+1), body))
  196. errs = append(errs, err)
  197. return err == nil
  198. }
  199. if add(wire.FeedbagDeleteItem, deletes, wire.SNAC_0x13_0x0A_FeedbagDeleteItem{Items: deletes}, deleteErr) &&
  200. add(wire.FeedbagUpdateItem, updates, wire.SNAC_0x13_0x09_FeedbagUpdateItem{Items: updates}, updateErr) {
  201. add(wire.FeedbagInsertItem, inserts, wire.SNAC_0x13_0x08_FeedbagInsertItem{Items: inserts}, insertErr)
  202. }
  203. return want, errs
  204. }
  205. // matchSession matches a mock call based session ident screen name.
  206. func matchSession(mustMatch state.IdentScreenName) interface{} {
  207. return mock.MatchedBy(func(s *state.SessionInstance) bool {
  208. return mustMatch == s.IdentScreenName()
  209. })
  210. }
  211. // newTestSession creates a session object with 0 or more functional options
  212. // applied
  213. func newTestSession(screenName state.DisplayScreenName, options ...func(instance *state.SessionInstance)) *state.SessionInstance {
  214. s := state.NewSession().AddInstance()
  215. s.Session().SetIdentScreenName(screenName.IdentScreenName())
  216. s.Session().SetDisplayScreenName(screenName)
  217. s.SetSignonComplete()
  218. s.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses([5]wire.RateClass{
  219. {
  220. ID: 1,
  221. WindowSize: 80,
  222. ClearLevel: 2500,
  223. AlertLevel: 2000,
  224. LimitLevel: 1500,
  225. DisconnectLevel: 800,
  226. MaxLevel: 6000,
  227. },
  228. {
  229. ID: 2,
  230. WindowSize: 80,
  231. ClearLevel: 3000,
  232. AlertLevel: 2000,
  233. LimitLevel: 1500,
  234. DisconnectLevel: 1000,
  235. MaxLevel: 6000,
  236. },
  237. {
  238. ID: 3,
  239. WindowSize: 20,
  240. ClearLevel: 5100,
  241. AlertLevel: 5000,
  242. LimitLevel: 4000,
  243. DisconnectLevel: 3000,
  244. MaxLevel: 6000,
  245. },
  246. {
  247. ID: 4,
  248. WindowSize: 20,
  249. ClearLevel: 5500,
  250. AlertLevel: 5300,
  251. LimitLevel: 4200,
  252. DisconnectLevel: 3000,
  253. MaxLevel: 8000,
  254. },
  255. {
  256. ID: 5,
  257. WindowSize: 10,
  258. ClearLevel: 5500,
  259. AlertLevel: 5300,
  260. LimitLevel: 4200,
  261. DisconnectLevel: 3000,
  262. MaxLevel: 8000,
  263. },
  264. }))
  265. for _, op := range options {
  266. op(s)
  267. }
  268. return s
  269. }
  270. // matchContext matches any instance of Context interface.
  271. func matchContext() interface{} {
  272. return mock.MatchedBy(func(ctx any) bool {
  273. _, ok := ctx.(context.Context)
  274. return ok
  275. })
  276. }
  277. // receivedSNAC is a SNAC that a fake OSCAR service received from TOC, with
  278. // its body still marshaled.
  279. type receivedSNAC struct {
  280. frame wire.SNACFrame
  281. body []byte
  282. }
  283. // newReceivedSNAC returns the receivedSNAC for a SNAC TOC sends as its
  284. // reqIDth request.
  285. func newReceivedSNAC(t *testing.T, foodGroup uint16, subGroup uint16, reqID uint32, body any) receivedSNAC {
  286. buf := &bytes.Buffer{}
  287. assert.NoError(t, wire.MarshalBE(body, buf))
  288. return receivedSNAC{
  289. frame: wire.SNACFrame{FoodGroup: foodGroup, SubGroup: subGroup, RequestID: reqID},
  290. body: buf.Bytes(),
  291. }
  292. }
  293. // fakeOSCARServer is an OSCARServer that serves each connection TOC opens
  294. // with the next of its handlers, in order.
  295. type fakeOSCARServer struct {
  296. mu sync.Mutex
  297. handlers []func(conn net.Conn)
  298. clientAddrs []net.Addr
  299. }
  300. func (s *fakeOSCARServer) ServeConn(conn net.Conn, _ config.Endpoint) {
  301. defer conn.Close()
  302. s.mu.Lock()
  303. s.clientAddrs = append(s.clientAddrs, conn.RemoteAddr())
  304. if len(s.handlers) == 0 {
  305. s.mu.Unlock()
  306. return
  307. }
  308. handler := s.handlers[0]
  309. s.handlers = s.handlers[1:]
  310. s.mu.Unlock()
  311. handler(conn)
  312. }
  313. // fakeAuth returns an auth service handler that records the client's signon
  314. // frame in got and replies with replyTLVs.
  315. func fakeAuth(got *wire.FLAPSignonFrame, replyTLVs ...wire.TLV) func(conn net.Conn) {
  316. return func(conn net.Conn) {
  317. flapc := wire.NewFlapClient(100, conn, conn)
  318. if err := flapc.SendSignonFrame(nil); err != nil {
  319. return
  320. }
  321. frame, err := flapc.ReceiveSignonFrame()
  322. if err != nil {
  323. return
  324. }
  325. *got = frame
  326. _ = flapc.NewSignoff(wire.TLVRestBlock{TLVList: replyTLVs})
  327. }
  328. }
  329. // fakeAuthBUCP returns an auth service handler that records the client's
  330. // signon frame in got and replies with a BUCP login response holding
  331. // replyTLVs, as the OSCAR server does when it rate limits an address that
  332. // recently used BUCP login.
  333. func fakeAuthBUCP(got *wire.FLAPSignonFrame, replyTLVs ...wire.TLV) func(conn net.Conn) {
  334. return func(conn net.Conn) {
  335. flapc := wire.NewFlapClient(100, conn, conn)
  336. if err := flapc.SendSignonFrame(nil); err != nil {
  337. return
  338. }
  339. frame, err := flapc.ReceiveSignonFrame()
  340. if err != nil {
  341. return
  342. }
  343. *got = frame
  344. _ = flapc.SendSNAC(wire.SNACFrame{FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse},
  345. wire.SNAC_0x17_0x03_BUCPLoginResponse{TLVRestBlock: wire.TLVRestBlock{TLVList: replyTLVs}})
  346. }
  347. }
  348. // fakeService is the server side of a BOS or chat connection opened by TOC. It
  349. // records the login cookie and the SNACs TOC sends, and answers each SNAC with
  350. // reply.
  351. type fakeService struct {
  352. // refuse makes the service sign the client off instead of going online.
  353. refuse bool
  354. // greeting are SNACs sent right after the service goes online.
  355. greeting []wire.SNACMessage
  356. // reply returns the SNACs that answer a SNAC from TOC.
  357. reply func(frame wire.SNACFrame) []wire.SNACMessage
  358. // ended, if set, is closed once TOC closes the connection.
  359. ended chan struct{}
  360. mu sync.Mutex
  361. cookie []byte
  362. snacs []receivedSNAC
  363. }
  364. func (f *fakeService) handle(conn net.Conn) {
  365. if f.ended != nil {
  366. defer close(f.ended)
  367. }
  368. flapc := wire.NewFlapClient(100, conn, conn)
  369. if err := flapc.SendSignonFrame(nil); err != nil {
  370. return
  371. }
  372. signon, err := flapc.ReceiveSignonFrame()
  373. if err != nil {
  374. return
  375. }
  376. f.mu.Lock()
  377. f.cookie, _ = signon.Bytes(wire.OServiceTLVTagsLoginCookie)
  378. f.mu.Unlock()
  379. if f.refuse {
  380. _ = flapc.NewSignoff(wire.TLVRestBlock{})
  381. return
  382. }
  383. if err := flapc.SendSNAC(wire.SNACFrame{FoodGroup: wire.OService, SubGroup: wire.OServiceHostOnline},
  384. wire.SNAC_0x01_0x03_OServiceHostOnline{}); err != nil {
  385. return
  386. }
  387. for _, msg := range f.greeting {
  388. if err := flapc.SendSNAC(msg.Frame, msg.Body); err != nil {
  389. return
  390. }
  391. }
  392. for {
  393. flap, err := flapc.ReceiveFLAP()
  394. if err != nil || flap.FrameType != wire.FLAPFrameData {
  395. return
  396. }
  397. r := bytes.NewReader(flap.Payload)
  398. frame := wire.SNACFrame{}
  399. if err := wire.UnmarshalBE(&frame, r); err != nil {
  400. return
  401. }
  402. body, _ := io.ReadAll(r)
  403. if len(body) == 0 {
  404. body = nil
  405. }
  406. f.mu.Lock()
  407. f.snacs = append(f.snacs, receivedSNAC{frame: frame, body: body})
  408. f.mu.Unlock()
  409. if f.reply == nil {
  410. continue
  411. }
  412. for _, msg := range f.reply(frame) {
  413. if err := flapc.SendSNAC(msg.Frame, msg.Body); err != nil {
  414. return
  415. }
  416. }
  417. }
  418. }
  419. // received returns the login cookie and the SNACs received so far.
  420. func (f *fakeService) received() ([]byte, []receivedSNAC) {
  421. f.mu.Lock()
  422. defer f.mu.Unlock()
  423. return f.cookie, append([]receivedSNAC(nil), f.snacs...)
  424. }