| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- package toc
- import (
- "bytes"
- "context"
- "errors"
- "io"
- "net"
- "sync"
- "testing"
- "time"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/mk6i/open-oscar-server/config"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- type userInfoQueryParams []struct {
- me state.IdentScreenName
- inBody wire.SNAC_0x02_0x05_LocateUserInfoQuery
- msg wire.SNACMessage
- err error
- }
- type dirInfoParams []struct {
- body wire.SNAC_0x02_0x0B_LocateGetDirInfo
- msg wire.SNACMessage
- err error
- }
- type locateParams struct {
- userInfoQueryParams
- dirInfoParams
- }
- type infoQueryParams []struct {
- inBody wire.SNAC_0x0F_0x02_InfoQuery
- msg wire.SNACMessage
- err error
- }
- type dirSearchParams struct {
- infoQueryParams
- }
- type setTOCConfigParams []struct {
- user state.IdentScreenName
- config string
- err error
- }
- type retrieveSessionParams []struct {
- screenName state.IdentScreenName
- returnedSession *state.Session
- }
- type sessionRetrieverParams struct {
- retrieveSessionParams
- }
- type tocConfigParams struct {
- setTOCConfigParams
- }
- type feedBagParams struct {
- feedbagParams
- upsertFeedbagParams
- deleteFeedbagParams
- }
- // feedbagParams holds the user's feedbag before a command runs.
- type feedbagParams []struct {
- results []wire.FeedbagItem
- }
- // upsertFeedbagParams is the list of feedbag insert or update SNACs a command
- // is expected to send, in order. frame holds the expected subgroup, items the
- // exact feedbag items, and err the error the connection returns for the send.
- type upsertFeedbagParams []struct {
- frame wire.SNACFrame
- items []wire.FeedbagItem
- err error
- }
- // deleteFeedbagParams is the list of feedbag delete SNACs a command is
- // expected to send, in order, before any upserts.
- type deleteFeedbagParams []struct {
- frame wire.SNACFrame
- inBody wire.SNAC_0x13_0x0A_FeedbagDeleteItem
- err error
- }
- type mockParams struct {
- cookieBakerParams
- dirSearchParams
- locateParams
- feedBagParams
- sessionRetrieverParams
- tocConfigParams
- }
- // issueParams holds multiple scenarios for the Issue method.
- type issueParams []struct {
- data []byte
- returnData []byte
- returnErr error
- }
- // cookieBakerParams groups the method scenarios for a CookieBaker.
- type cookieBakerParams struct {
- issueParams issueParams
- }
- // fakeOSCARConn is an oscarConn that records the SNACs sent to it. The nth
- // send returns sendErrs[n], if set. Tests feed server SNACs through msgs.
- type fakeOSCARConn struct {
- mu sync.Mutex
- sent []wire.SNACMessage
- sendErrs []error
- msgs chan wire.SNACMessage
- closed chan struct{}
- closeOnce sync.Once
- }
- func newFakeOSCARConn() *fakeOSCARConn {
- return &fakeOSCARConn{
- msgs: make(chan wire.SNACMessage),
- closed: make(chan struct{}),
- }
- }
- func (c *fakeOSCARConn) SendSNAC(frame wire.SNACFrame, body any) error {
- c.mu.Lock()
- defer c.mu.Unlock()
- n := len(c.sent)
- c.sent = append(c.sent, wire.SNACMessage{Frame: frame, Body: body})
- if n < len(c.sendErrs) {
- return c.sendErrs[n]
- }
- return nil
- }
- func (c *fakeOSCARConn) Messages() <-chan wire.SNACMessage {
- return c.msgs
- }
- func (c *fakeOSCARConn) Closed() <-chan struct{} {
- return c.closed
- }
- func (c *fakeOSCARConn) Close() {
- c.closeOnce.Do(func() {
- close(c.closed)
- })
- }
- func (c *fakeOSCARConn) isClosed() bool {
- select {
- case <-c.closed:
- return true
- default:
- return false
- }
- }
- // sentSNACs returns a copy of the SNACs sent so far.
- func (c *fakeOSCARConn) sentSNACs() []wire.SNACMessage {
- c.mu.Lock()
- defer c.mu.Unlock()
- return append([]wire.SNACMessage(nil), c.sent...)
- }
- // newTestTOCConn creates a TOC connection for the user of instance whose BOS
- // connection is a fakeOSCARConn.
- func newTestTOCConn(instance *state.SessionInstance) (*tocConn, *fakeOSCARConn) {
- bos := newFakeOSCARConn()
- me := newTOCConn(bos, instance.IdentScreenName(), &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234})
- me.toCh = make(chan []string, 16)
- me.goAsync = func(f func() error) { go f() }
- return me, bos
- }
- // newSNAC returns the SNAC message a TOC connection sends as its reqIDth
- // request.
- func newSNAC(foodGroup uint16, subGroup uint16, reqID uint32, body any) wire.SNACMessage {
- return wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: foodGroup,
- SubGroup: subGroup,
- RequestID: reqID,
- },
- Body: body,
- }
- }
- // feedbagSNACs returns the feedbag SNACs that params expect a command to send.
- // A command sends at most one SNAC per subgroup, in the order deletes,
- // updates, inserts, so params' items are combined by subgroup in table order.
- // It also returns the error for each send. Sends stop at the first error.
- func feedbagSNACs(params feedBagParams) ([]wire.SNACMessage, []error) {
- var deletes, updates, inserts []wire.FeedbagItem
- var deleteErr, updateErr, insertErr error
- for _, p := range params.deleteFeedbagParams {
- deletes = append(deletes, p.inBody.Items...)
- deleteErr = errors.Join(deleteErr, p.err)
- }
- for _, p := range params.upsertFeedbagParams {
- if p.frame.SubGroup == wire.FeedbagUpdateItem {
- updates = append(updates, p.items...)
- updateErr = errors.Join(updateErr, p.err)
- } else {
- inserts = append(inserts, p.items...)
- insertErr = errors.Join(insertErr, p.err)
- }
- }
- var want []wire.SNACMessage
- var errs []error
- add := func(subGroup uint16, items []wire.FeedbagItem, body any, err error) bool {
- if len(items) == 0 {
- return true
- }
- want = append(want, newSNAC(wire.Feedbag, subGroup, uint32(len(want)+1), body))
- errs = append(errs, err)
- return err == nil
- }
- if add(wire.FeedbagDeleteItem, deletes, wire.SNAC_0x13_0x0A_FeedbagDeleteItem{Items: deletes}, deleteErr) &&
- add(wire.FeedbagUpdateItem, updates, wire.SNAC_0x13_0x09_FeedbagUpdateItem{Items: updates}, updateErr) {
- add(wire.FeedbagInsertItem, inserts, wire.SNAC_0x13_0x08_FeedbagInsertItem{Items: inserts}, insertErr)
- }
- return want, errs
- }
- // matchSession matches a mock call based session ident screen name.
- func matchSession(mustMatch state.IdentScreenName) interface{} {
- return mock.MatchedBy(func(s *state.SessionInstance) bool {
- return mustMatch == s.IdentScreenName()
- })
- }
- // newTestSession creates a session object with 0 or more functional options
- // applied
- func newTestSession(screenName state.DisplayScreenName, options ...func(instance *state.SessionInstance)) *state.SessionInstance {
- s := state.NewSession().AddInstance()
- s.Session().SetIdentScreenName(screenName.IdentScreenName())
- s.Session().SetDisplayScreenName(screenName)
- s.SetSignonComplete()
- s.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses([5]wire.RateClass{
- {
- ID: 1,
- WindowSize: 80,
- ClearLevel: 2500,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 800,
- MaxLevel: 6000,
- },
- {
- ID: 2,
- WindowSize: 80,
- ClearLevel: 3000,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 1000,
- MaxLevel: 6000,
- },
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {
- ID: 4,
- WindowSize: 20,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- {
- ID: 5,
- WindowSize: 10,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- }))
- for _, op := range options {
- op(s)
- }
- return s
- }
- // matchContext matches any instance of Context interface.
- func matchContext() interface{} {
- return mock.MatchedBy(func(ctx any) bool {
- _, ok := ctx.(context.Context)
- return ok
- })
- }
- // receivedSNAC is a SNAC that a fake OSCAR service received from TOC, with
- // its body still marshaled.
- type receivedSNAC struct {
- frame wire.SNACFrame
- body []byte
- }
- // newReceivedSNAC returns the receivedSNAC for a SNAC TOC sends as its
- // reqIDth request.
- func newReceivedSNAC(t *testing.T, foodGroup uint16, subGroup uint16, reqID uint32, body any) receivedSNAC {
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(body, buf))
- return receivedSNAC{
- frame: wire.SNACFrame{FoodGroup: foodGroup, SubGroup: subGroup, RequestID: reqID},
- body: buf.Bytes(),
- }
- }
- // fakeOSCARServer is an OSCARServer that serves each connection TOC opens
- // with the next of its handlers, in order.
- type fakeOSCARServer struct {
- mu sync.Mutex
- handlers []func(conn net.Conn)
- clientAddrs []net.Addr
- }
- func (s *fakeOSCARServer) ServeConn(conn net.Conn, _ config.Endpoint) {
- defer conn.Close()
- s.mu.Lock()
- s.clientAddrs = append(s.clientAddrs, conn.RemoteAddr())
- if len(s.handlers) == 0 {
- s.mu.Unlock()
- return
- }
- handler := s.handlers[0]
- s.handlers = s.handlers[1:]
- s.mu.Unlock()
- handler(conn)
- }
- // fakeAuth returns an auth service handler that records the client's signon
- // frame in got and replies with replyTLVs.
- func fakeAuth(got *wire.FLAPSignonFrame, replyTLVs ...wire.TLV) func(conn net.Conn) {
- return func(conn net.Conn) {
- flapc := wire.NewFlapClient(100, conn, conn)
- if err := flapc.SendSignonFrame(nil); err != nil {
- return
- }
- frame, err := flapc.ReceiveSignonFrame()
- if err != nil {
- return
- }
- *got = frame
- _ = flapc.NewSignoff(wire.TLVRestBlock{TLVList: replyTLVs})
- }
- }
- // fakeAuthBUCP returns an auth service handler that records the client's
- // signon frame in got and replies with a BUCP login response holding
- // replyTLVs, as the OSCAR server does when it rate limits an address that
- // recently used BUCP login.
- func fakeAuthBUCP(got *wire.FLAPSignonFrame, replyTLVs ...wire.TLV) func(conn net.Conn) {
- return func(conn net.Conn) {
- flapc := wire.NewFlapClient(100, conn, conn)
- if err := flapc.SendSignonFrame(nil); err != nil {
- return
- }
- frame, err := flapc.ReceiveSignonFrame()
- if err != nil {
- return
- }
- *got = frame
- _ = flapc.SendSNAC(wire.SNACFrame{FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse},
- wire.SNAC_0x17_0x03_BUCPLoginResponse{TLVRestBlock: wire.TLVRestBlock{TLVList: replyTLVs}})
- }
- }
- // fakeService is the server side of a BOS or chat connection opened by TOC. It
- // records the login cookie and the SNACs TOC sends, and answers each SNAC with
- // reply.
- type fakeService struct {
- // refuse makes the service sign the client off instead of going online.
- refuse bool
- // greeting are SNACs sent right after the service goes online.
- greeting []wire.SNACMessage
- // reply returns the SNACs that answer a SNAC from TOC.
- reply func(frame wire.SNACFrame) []wire.SNACMessage
- // ended, if set, is closed once TOC closes the connection.
- ended chan struct{}
- mu sync.Mutex
- cookie []byte
- snacs []receivedSNAC
- }
- func (f *fakeService) handle(conn net.Conn) {
- if f.ended != nil {
- defer close(f.ended)
- }
- flapc := wire.NewFlapClient(100, conn, conn)
- if err := flapc.SendSignonFrame(nil); err != nil {
- return
- }
- signon, err := flapc.ReceiveSignonFrame()
- if err != nil {
- return
- }
- f.mu.Lock()
- f.cookie, _ = signon.Bytes(wire.OServiceTLVTagsLoginCookie)
- f.mu.Unlock()
- if f.refuse {
- _ = flapc.NewSignoff(wire.TLVRestBlock{})
- return
- }
- if err := flapc.SendSNAC(wire.SNACFrame{FoodGroup: wire.OService, SubGroup: wire.OServiceHostOnline},
- wire.SNAC_0x01_0x03_OServiceHostOnline{}); err != nil {
- return
- }
- for _, msg := range f.greeting {
- if err := flapc.SendSNAC(msg.Frame, msg.Body); err != nil {
- return
- }
- }
- for {
- flap, err := flapc.ReceiveFLAP()
- if err != nil || flap.FrameType != wire.FLAPFrameData {
- return
- }
- r := bytes.NewReader(flap.Payload)
- frame := wire.SNACFrame{}
- if err := wire.UnmarshalBE(&frame, r); err != nil {
- return
- }
- body, _ := io.ReadAll(r)
- if len(body) == 0 {
- body = nil
- }
- f.mu.Lock()
- f.snacs = append(f.snacs, receivedSNAC{frame: frame, body: body})
- f.mu.Unlock()
- if f.reply == nil {
- continue
- }
- for _, msg := range f.reply(frame) {
- if err := flapc.SendSNAC(msg.Frame, msg.Body); err != nil {
- return
- }
- }
- }
- }
- // received returns the login cookie and the SNACs received so far.
- func (f *fakeService) received() ([]byte, []receivedSNAC) {
- f.mu.Lock()
- defer f.mu.Unlock()
- return f.cookie, append([]receivedSNAC(nil), f.snacs...)
- }
|