| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199 |
- package oscar
- import (
- "bufio"
- "bytes"
- "context"
- "io"
- "log/slog"
- "net"
- "strings"
- "sync"
- "testing"
- "time"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "golang.org/x/time/rate"
- "github.com/mk6i/open-oscar-server/config"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- func TestServer_ListenAndServeAndShutdown(t *testing.T) {
- var mu sync.Mutex
- var received []string
- var msgWg sync.WaitGroup
- cfg := []config.Listener{
- {
- BOSListenAddress: ":15000",
- BOSAdvertisedHostPlain: "localhost",
- },
- {
- BOSListenAddress: ":15001",
- BOSAdvertisedHostPlain: "localhost",
- },
- {
- BOSListenAddress: ":15002",
- BOSAdvertisedHostPlain: "localhost",
- },
- }
- responses := []string{"hello1", "hello2", "hello2"}
- server := NewServer(
- nil,
- nil,
- nil,
- nil,
- slog.Default(),
- nil,
- nil,
- nil,
- wire.DefaultSNACRateLimits(),
- nil,
- cfg,
- func(ctx context.Context, instance *state.SessionInstance) error { return nil },
- func(ctx context.Context, instance *state.SessionInstance) {},
- )
- server.handler = func(ctx context.Context, conn net.Conn, listener config.Listener) error {
- go func() {
- <-ctx.Done()
- _ = conn.Close()
- }()
- for {
- r := bufio.NewReader(conn)
- line, err := r.ReadString('\n')
- if err != nil {
- break
- }
- mu.Lock()
- received = append(received, strings.TrimSpace(line))
- mu.Unlock()
- msgWg.Done()
- }
- return nil
- }
- server.shutdownCtx, server.shutdownCancel = context.WithCancel(context.Background())
- shutdownCh := make(chan struct{})
- go func() {
- defer close(shutdownCh)
- assert.NoError(t, server.ListenAndServe())
- }()
- // Wait for server to be ready by checking if ports are listening
- for i := 0; i < len(cfg); i++ {
- maxRetries := 10
- backoff := 5 * time.Millisecond
- for attempt := 0; attempt < maxRetries; attempt++ {
- conn, err := net.Dial("tcp", "localhost"+cfg[i].BOSListenAddress)
- if err == nil {
- _ = conn.Close()
- break
- }
- if attempt == maxRetries-1 {
- t.Fatalf("Server not ready after %d attempts: %v", maxRetries, err)
- }
- time.Sleep(backoff)
- backoff *= 2
- }
- }
- for i := 0; i < len(cfg); i++ {
- msgWg.Add(1)
- // Connect and send message
- conn, err := net.Dial("tcp", "localhost"+cfg[i].BOSListenAddress)
- assert.NoError(t, err)
- _, err = conn.Write([]byte(responses[i] + "\n"))
- assert.NoError(t, err)
- }
- msgWg.Wait()
- // Shutdown
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- err := server.Shutdown(ctx)
- assert.NoError(t, err)
- <-shutdownCh
- // Check what was received
- mu.Lock()
- defer mu.Unlock()
- assert.ElementsMatch(t, received, responses)
- }
- type fakeConn struct {
- net.Conn // embed the real connection
- local net.Addr
- remote net.Addr
- }
- func (f fakeConn) RemoteAddr() net.Addr { return f.remote }
- func TestOscarServer_RouteConnection_Auth_BUCP(t *testing.T) {
- serverConn, clientConn := net.Pipe()
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{
- Conn: serverConn,
- local: addr,
- remote: addr,
- }
- go func() {
- defer func() {
- _ = clientConn.Close()
- }()
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // > send FLAPSignonFrame
- flapSignonFrame = wire.FLAPSignonFrame{
- FLAPVersion: 1,
- }
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(flapSignonFrame, buf))
- flap = wire.FLAPFrame{
- StartMarker: 42,
- FrameType: wire.FLAPFrameSignon,
- Payload: buf.Bytes(),
- }
- assert.NoError(t, wire.MarshalBE(flap, clientConn))
- // > send SNAC_0x17_0x06_BUCPChallengeRequest
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- frame := wire.SNACFrame{
- FoodGroup: wire.BUCP,
- SubGroup: wire.BUCPChallengeRequest,
- RequestID: 4,
- }
- bodyIn := wire.SNAC_0x17_0x06_BUCPChallengeRequest{}
- assert.NoError(t, flapc.SendSNAC(frame, bodyIn))
- // < receive SNAC_0x17_0x07_BUCPChallengeResponse
- frame = wire.SNACFrame{}
- assert.NoError(t, flapc.ReceiveSNAC(&frame, &wire.SNAC_0x17_0x07_BUCPChallengeResponse{}))
- assert.Equal(t, wire.SNACFrame{FoodGroup: wire.BUCP, SubGroup: wire.BUCPChallengeResponse, RequestID: 4}, frame)
- // > send keep alive frame (like BSFlite does mid-login)
- assert.NoError(t, flapc.SendKeepAliveFrame())
- // > send SNAC_0x17_0x02_BUCPLoginRequest
- frame = wire.SNACFrame{
- FoodGroup: wire.BUCP,
- SubGroup: wire.BUCPLoginRequest,
- RequestID: 5,
- }
- assert.NoError(t, flapc.SendSNAC(frame, wire.SNAC_0x17_0x02_BUCPLoginRequest{}))
- // < receive SNAC_0x17_0x03_BUCPLoginResponse
- frame = wire.SNACFrame{}
- assert.NoError(t, flapc.ReceiveSNAC(&frame, &wire.SNAC_0x17_0x03_BUCPLoginResponse{}))
- assert.Equal(t, wire.SNACFrame{FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, RequestID: 5}, frame)
- // < receive FLAPSignoffFrame (server sends this after SNAC for Kopete compatibility)
- flap = wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- assert.Equal(t, wire.FLAPFrameSignoff, flap.FrameType)
- }()
- wg := &sync.WaitGroup{}
- authService := newMockAuthService(t)
- authService.EXPECT().
- BUCPChallenge(matchContext(), mock.Anything, mock.Anything).
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.BUCP,
- SubGroup: wire.BUCPChallengeResponse,
- },
- Body: wire.SNAC_0x17_0x07_BUCPChallengeResponse{},
- }, nil)
- authService.EXPECT().
- BUCPLogin(matchContext(), mock.Anything, "localhost:5190").
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.BUCP,
- SubGroup: wire.BUCPLoginResponse,
- },
- Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{},
- }, nil)
- rt := oscarServer{
- authService: authService,
- logger: slog.Default(),
- ipRateLimiter: NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
- }
- assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{BOSAdvertisedHostPlain: "localhost:5190"}))
- wg.Wait()
- }
- func TestOscarServer_RouteConnection_Auth_FLAP(t *testing.T) {
- serverConn, clientConn := net.Pipe()
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{
- Conn: serverConn,
- local: addr,
- remote: addr,
- }
- go func() {
- defer func() {
- _ = clientConn.Close()
- }()
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // > send FLAPSignonFrame with screen name TLV (indicates FLAP auth)
- flapSignonFrame = wire.FLAPSignonFrame{
- FLAPVersion: 1,
- }
- // Add screen name TLV to indicate FLAP authentication
- flapSignonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsScreenName, "testuser"))
- // Add password hash TLV for authentication
- flapSignonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, []byte("password_hash")))
- // Add client identity TLV
- flapSignonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"))
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(flapSignonFrame, buf))
- flap = wire.FLAPFrame{
- StartMarker: 42,
- FrameType: wire.FLAPFrameSignon,
- Payload: buf.Bytes(),
- }
- assert.NoError(t, wire.MarshalBE(flap, clientConn))
- // < receive FLAPSignoffFrame with authentication result
- flap = wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- assert.Equal(t, wire.FLAPFrameSignoff, flap.FrameType)
- // Parse the signoff frame payload to verify authentication response
- signoffTLVs := wire.TLVRestBlock{}
- assert.NoError(t, wire.UnmarshalBE(&signoffTLVs, bytes.NewBuffer(flap.Payload)))
- }()
- wg := &sync.WaitGroup{}
- authService := newMockAuthService(t)
- authService.EXPECT().
- FLAPLogin(matchContext(), mock.Anything, "localhost:5190").
- Return(wire.TLVRestBlock{
- TLVList: []wire.TLV{
- wire.NewTLVBE(wire.LoginTLVTagsScreenName, "testuser"),
- wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "localhost:5190"),
- wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("auth-cookie")),
- },
- }, nil)
- rt := oscarServer{
- authService: authService,
- logger: slog.Default(),
- ipRateLimiter: NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
- }
- assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{BOSAdvertisedHostPlain: "localhost:5190"}))
- wg.Wait()
- }
- func TestOscarServer_RouteConnection_BOS(t *testing.T) {
- instance := state.NewSession().AddInstance()
- clientConn, serverConn := net.Pipe()
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{
- Conn: serverConn,
- local: addr,
- remote: addr,
- }
- go func() {
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // > send FLAPSignonFrame
- flapSignonFrame = wire.FLAPSignonFrame{
- FLAPVersion: 1,
- }
- flapSignonFrame.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")))
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(flapSignonFrame, buf))
- flap = wire.FLAPFrame{
- StartMarker: 42,
- FrameType: wire.FLAPFrameSignon,
- Payload: buf.Bytes(),
- }
- assert.NoError(t, wire.MarshalBE(flap, clientConn))
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- // < receive SNAC_0x01_0x03_OServiceHostOnline
- frame := wire.SNACFrame{}
- body := wire.SNAC_0x01_0x03_OServiceHostOnline{}
- assert.NoError(t, flapc.ReceiveSNAC(&frame, &body))
- // send the first request that should get relayed to BOSRouter.Handle
- frame = wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceClientOnline,
- }
- assert.NoError(t, flapc.SendSNAC(frame, struct{}{}))
- }()
- wg := &sync.WaitGroup{}
- authService := newMockAuthService(t)
- authService.EXPECT().
- RegisterBOSSession(mock.Anything, state.ServerCookie{Service: wire.BOS}, mock.Anything).
- Run(func(ctx context.Context, cookie state.ServerCookie, conf func(*state.Session)) {
- if conf != nil {
- conf(instance.Session())
- }
- }).
- Return(instance, nil)
- wg.Add(1)
- authService.EXPECT().
- Signout(mock.Anything, instance.Session()).
- Run(func(ctx context.Context, s *state.Session) {
- defer wg.Done()
- })
- authService.EXPECT().
- CrackCookie(mock.Anything).
- Return(state.ServerCookie{Service: wire.BOS}, nil)
- onlineNotifier := newMockOnlineNotifier(t)
- onlineNotifier.EXPECT().
- HostOnline(mock.Anything).
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{},
- })
- buddyListRegistry := newMockBuddyListRegistry(t)
- buddyListRegistry.EXPECT().
- RegisterBuddyList(mock.Anything, mock.Anything).
- Return(nil)
- buddyListRegistry.EXPECT().
- UnregisterBuddyList(mock.Anything, mock.Anything).
- Return(nil)
- departureNotifier := newMockDepartureNotifier(t)
- departureNotifier.EXPECT().
- BroadcastBuddyDeparted(mock.Anything, mock.Anything).
- Return(nil)
- chatSessionManager := newMockChatSessionManager(t)
- chatSessionManager.EXPECT().
- RemoveUserFromAllChats(mock.Anything)
- wg.Add(2)
- handler := func(ctx context.Context, serverType uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, r io.Reader, rw ResponseWriter, listener config.Listener) error {
- defer wg.Done()
- assert.NoError(t, clientConn.Close())
- return nil
- }
- rt := oscarServer{
- authService: authService,
- snacHandler: handler,
- logger: slog.Default(),
- onlineNotifier: onlineNotifier,
- buddyListRegistry: buddyListRegistry,
- chatSessionManager: chatSessionManager,
- departureNotifier: departureNotifier,
- recalcWarning: func(ctx context.Context, instance *state.SessionInstance) error {
- return nil
- },
- lowerWarnLevel: func(ctx context.Context, instance *state.SessionInstance) {
- defer wg.Done()
- },
- }
- assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{}))
- wg.Wait()
- }
- // Set up a multi-instance session and connect as the second instance.
- // - Ensure that disconnecting second instance does not sign out the session.
- func TestOscarServer_RouteConnection_BOS_MultiSessionSignoff(t *testing.T) {
- instance := state.NewSession().AddInstance()
- instance.Session().AddInstance()
- clientConn, serverConn := net.Pipe()
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{
- Conn: serverConn,
- local: addr,
- remote: addr,
- }
- go func() {
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // > send FLAPSignonFrame
- flapSignonFrame = wire.FLAPSignonFrame{
- FLAPVersion: 1,
- }
- flapSignonFrame.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")))
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(flapSignonFrame, buf))
- flap = wire.FLAPFrame{
- StartMarker: 42,
- FrameType: wire.FLAPFrameSignon,
- Payload: buf.Bytes(),
- }
- assert.NoError(t, wire.MarshalBE(flap, clientConn))
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- // < receive SNAC_0x01_0x03_OServiceHostOnline
- frame := wire.SNACFrame{}
- body := wire.SNAC_0x01_0x03_OServiceHostOnline{}
- assert.NoError(t, flapc.ReceiveSNAC(&frame, &body))
- // send the first request that should get relayed to BOSRouter.Handle
- frame = wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceClientOnline,
- }
- assert.NoError(t, flapc.SendSNAC(frame, struct{}{}))
- }()
- wg := &sync.WaitGroup{}
- authService := newMockAuthService(t)
- authService.EXPECT().
- RegisterBOSSession(mock.Anything, state.ServerCookie{Service: wire.BOS}, mock.Anything).
- Return(instance, nil)
- authService.EXPECT().
- CrackCookie(mock.Anything).
- Return(state.ServerCookie{Service: wire.BOS}, nil)
- onlineNotifier := newMockOnlineNotifier(t)
- onlineNotifier.EXPECT().
- HostOnline(mock.Anything).
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{},
- })
- buddyListRegistry := newMockBuddyListRegistry(t)
- buddyListRegistry.EXPECT().
- RegisterBuddyList(mock.Anything, mock.Anything).
- Return(nil)
- departureNotifier := newMockDepartureNotifier(t)
- departureNotifier.EXPECT().
- BroadcastBuddyArrived(mock.Anything, mock.Anything, mock.Anything).
- Return(nil)
- chatSessionManager := newMockChatSessionManager(t)
- wg.Add(2)
- handler := func(ctx context.Context, serverType uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, r io.Reader, rw ResponseWriter, listener config.Listener) error {
- defer wg.Done()
- assert.NoError(t, clientConn.Close())
- return nil
- }
- rt := oscarServer{
- authService: authService,
- snacHandler: handler,
- logger: slog.Default(),
- onlineNotifier: onlineNotifier,
- buddyListRegistry: buddyListRegistry,
- chatSessionManager: chatSessionManager,
- departureNotifier: departureNotifier,
- recalcWarning: func(ctx context.Context, instance *state.SessionInstance) error {
- return nil
- },
- lowerWarnLevel: func(ctx context.Context, instance *state.SessionInstance) {
- defer wg.Done()
- },
- }
- assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{}))
- wg.Wait()
- }
- // Ensure client disconnection if session hits max concurrent sessions limit.
- func TestOscarServer_RouteConnection_BOS_MaxConcurrentSessionsReached(t *testing.T) {
- clientConn, serverConn := net.Pipe()
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{
- Conn: serverConn,
- local: addr,
- remote: addr,
- }
- go func() {
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // > send FLAPSignonFrame
- flapSignonFrame = wire.FLAPSignonFrame{
- FLAPVersion: 1,
- }
- flapSignonFrame.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")))
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(flapSignonFrame, buf))
- flap = wire.FLAPFrame{
- StartMarker: 42,
- FrameType: wire.FLAPFrameSignon,
- Payload: buf.Bytes(),
- }
- assert.NoError(t, wire.MarshalBE(flap, clientConn))
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- // < receive SNAC_0x01_0x03_OServiceHostOnline
- flap, err = flapc.ReceiveFLAP()
- assert.NoError(t, err)
- assert.NoError(t, clientConn.Close())
- }()
- wg := &sync.WaitGroup{}
- authService := newMockAuthService(t)
- authService.EXPECT().
- RegisterBOSSession(mock.Anything, state.ServerCookie{Service: wire.BOS}, mock.Anything).
- Return(nil, state.ErrMaxConcurrentSessionsReached)
- authService.EXPECT().
- CrackCookie(mock.Anything).
- Return(state.ServerCookie{Service: wire.BOS}, nil)
- rt := oscarServer{
- authService: authService,
- logger: slog.Default(),
- }
- assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{}))
- wg.Wait()
- }
- func TestOscarServer_RouteConnection_Chat(t *testing.T) {
- instance := state.NewSession().AddInstance()
- clientConn, serverConn := net.Pipe()
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{
- Conn: serverConn,
- local: addr,
- remote: addr,
- }
- go func() {
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // > send FLAPSignonFrame
- flapSignonFrame = wire.FLAPSignonFrame{
- FLAPVersion: 1,
- }
- flapSignonFrame.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")))
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(flapSignonFrame, buf))
- flap = wire.FLAPFrame{
- StartMarker: 42,
- FrameType: wire.FLAPFrameSignon,
- Payload: buf.Bytes(),
- }
- assert.NoError(t, wire.MarshalBE(flap, clientConn))
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- // < receive SNAC_0x01_0x03_OServiceHostOnline
- frame := wire.SNACFrame{}
- body := wire.SNAC_0x01_0x03_OServiceHostOnline{}
- assert.NoError(t, flapc.ReceiveSNAC(&frame, &body))
- // send the first request that should get relayed to BOSRouter.Handle
- frame = wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceClientOnline,
- }
- assert.NoError(t, flapc.SendSNAC(frame, struct{}{}))
- }()
- wg := &sync.WaitGroup{}
- authService := newMockAuthService(t)
- authService.EXPECT().
- RegisterChatSession(mock.Anything, state.ServerCookie{Service: wire.Chat}, mock.Anything).
- Run(func(_ context.Context, _ state.ServerCookie, cfg func(*state.Session)) {
- if cfg != nil {
- cfg(instance.Session())
- }
- }).
- Return(instance, nil)
- wg.Add(1)
- authService.EXPECT().
- SignoutChat(mock.Anything, instance.Session()).
- Run(func(ctx context.Context, s *state.Session) {
- defer wg.Done()
- })
- authService.EXPECT().
- CrackCookie(mock.Anything).
- Return(state.ServerCookie{Service: wire.Chat}, nil)
- onlineNotifier := newMockOnlineNotifier(t)
- onlineNotifier.EXPECT().
- HostOnline(mock.Anything).
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{},
- })
- buddyListRegistry := newMockBuddyListRegistry(t)
- departureNotifier := newMockDepartureNotifier(t)
- chatSessionManager := newMockChatSessionManager(t)
- wg.Add(1)
- handler := func(ctx context.Context, serverType uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, r io.Reader, rw ResponseWriter, listener config.Listener) error {
- defer wg.Done()
- assert.NoError(t, clientConn.Close())
- return nil
- }
- rt := oscarServer{
- authService: authService,
- snacHandler: handler,
- logger: slog.Default(),
- onlineNotifier: onlineNotifier,
- buddyListRegistry: buddyListRegistry,
- chatSessionManager: chatSessionManager,
- departureNotifier: departureNotifier,
- }
- assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{}))
- wg.Wait()
- }
- func TestOscarServer_RouteConnection_Admin(t *testing.T) {
- instance := state.NewSession().AddInstance()
- clientConn, serverConn := net.Pipe()
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{
- Conn: serverConn,
- local: addr,
- remote: addr,
- }
- go func() {
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // > send FLAPSignonFrame
- flapSignonFrame = wire.FLAPSignonFrame{
- FLAPVersion: 1,
- }
- flapSignonFrame.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")))
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(flapSignonFrame, buf))
- flap = wire.FLAPFrame{
- StartMarker: 42,
- FrameType: wire.FLAPFrameSignon,
- Payload: buf.Bytes(),
- }
- assert.NoError(t, wire.MarshalBE(flap, clientConn))
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- // < receive SNAC_0x01_0x03_OServiceHostOnline
- frame := wire.SNACFrame{}
- body := wire.SNAC_0x01_0x03_OServiceHostOnline{}
- assert.NoError(t, flapc.ReceiveSNAC(&frame, &body))
- // send the first request that should get relayed to BOSRouter.Handle
- frame = wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceClientOnline,
- }
- assert.NoError(t, flapc.SendSNAC(frame, struct{}{}))
- }()
- wg := &sync.WaitGroup{}
- authService := newMockAuthService(t)
- authService.EXPECT().
- CrackCookie(mock.Anything).
- Return(state.ServerCookie{Service: wire.Admin}, nil)
- authService.EXPECT().
- RetrieveBOSSession(mock.Anything, state.ServerCookie{Service: wire.Admin}).
- Return(instance, nil)
- onlineNotifier := newMockOnlineNotifier(t)
- onlineNotifier.EXPECT().
- HostOnline(mock.Anything).
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{},
- })
- buddyListRegistry := newMockBuddyListRegistry(t)
- departureNotifier := newMockDepartureNotifier(t)
- chatSessionManager := newMockChatSessionManager(t)
- wg.Add(1)
- handler := func(ctx context.Context, serverType uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, r io.Reader, rw ResponseWriter, listener config.Listener) error {
- defer wg.Done()
- assert.NoError(t, clientConn.Close())
- return nil
- }
- rt := oscarServer{
- authService: authService,
- snacHandler: handler,
- logger: slog.Default(),
- onlineNotifier: onlineNotifier,
- buddyListRegistry: buddyListRegistry,
- chatSessionManager: chatSessionManager,
- departureNotifier: departureNotifier,
- }
- assert.NoError(t, rt.routeConnection(context.Background(), clientFake, config.Listener{}))
- wg.Wait()
- }
- // Make sure the client receives signoff FLAP when the server shuts down via
- // context cancellation.
- func Test_oscarServer_dispatchIncomingMessages_shutdownSignoff(t *testing.T) {
- clientConn, serverConn := net.Pipe()
- ctx, cancel := context.WithCancel(context.Background())
- var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
- srv := oscarServer{
- logger: slog.Default(),
- }
- instance := state.NewSession().AddInstance()
- instance.SetMultiConnFlag(wire.MultiConnFlagsRecentClient)
- flapc := wire.NewFlapClient(0, serverConn, serverConn)
- err := srv.dispatchIncomingMessages(ctx, wire.BOS, instance, flapc, serverConn, config.Listener{})
- assert.NoError(t, err)
- }()
- cancel()
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- frame, err := flapc.ReceiveFLAP()
- assert.NoError(t, err)
- assert.Equal(t, wire.FLAPFrameSignoff, frame.FrameType)
- wg.Wait()
- }
- // Make sure the client (which doesn't support multi-conn) receives
- // disconnection signoff FLAP when the session gets logged off by a new session.
- func Test_oscarServer_dispatchIncomingMessages_disconnect_old_client(t *testing.T) {
- clientConn, serverConn := net.Pipe()
- ctx := context.Background()
- instance := state.NewSession().AddInstance()
- instance.SetMultiConnFlag(wire.MultiConnFlagsOldClient)
- var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
- srv := oscarServer{
- logger: slog.Default(),
- }
- flapc := wire.NewFlapClient(0, serverConn, serverConn)
- err := srv.dispatchIncomingMessages(ctx, wire.BOS, instance, flapc, serverConn, config.Listener{})
- assert.NoError(t, err)
- }()
- instance.CloseInstance()
- frame := wire.FLAPFrameDisconnect{}
- assert.NoError(t, wire.UnmarshalBE(&frame, clientConn))
- assert.Equal(t, wire.FLAPFrameSignoff, frame.FrameType)
- wg.Wait()
- }
- // Make sure the client (which supports multi-conn) receives disconnection
- // signoff FLAP when the session gets logged off by a new session.
- func Test_oscarServer_dispatchIncomingMessages_disconnect_new_client(t *testing.T) {
- clientConn, serverConn := net.Pipe()
- ctx := context.Background()
- instance := state.NewSession().AddInstance()
- instance.SetMultiConnFlag(wire.MultiConnFlagsRecentClient)
- var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
- srv := oscarServer{
- logger: slog.Default(),
- }
- flapc := wire.NewFlapClient(0, serverConn, serverConn)
- err := srv.dispatchIncomingMessages(ctx, wire.BOS, instance, flapc, serverConn, config.Listener{})
- assert.NoError(t, err)
- }()
- instance.CloseInstance()
- flapc := wire.NewFlapClient(0, clientConn, clientConn)
- frame, err := flapc.ReceiveFLAP()
- assert.NoError(t, err)
- assert.Equal(t, wire.FLAPFrameSignoff, frame.FrameType)
- wg.Wait()
- }
- func Test_oscarServer_receiveSessMessages_BOS_integration(t *testing.T) {
- serverConn, clientConn := net.Pipe()
- defer func() { _ = serverConn.Close() }()
- defer func() { _ = clientConn.Close() }()
- // Prepare session and mocks so we can exercise through routeConnection
- instance := state.NewSession().AddInstance()
- instance.SetSignonComplete()
- authService := newMockAuthService(t)
- authService.EXPECT().
- CrackCookie(mock.Anything).
- Return(state.ServerCookie{Service: wire.BOS}, nil)
- authService.EXPECT().
- RegisterBOSSession(mock.Anything, state.ServerCookie{Service: wire.BOS}, mock.Anything).
- Run(func(ctx context.Context, cookie state.ServerCookie, conf func(*state.Session)) {
- if conf != nil {
- conf(instance.Session())
- }
- }).
- Return(instance, nil)
- var signoutWG sync.WaitGroup
- signoutWG.Add(1)
- authService.EXPECT().
- Signout(mock.Anything, instance.Session()).
- Run(func(ctx context.Context, s *state.Session) { signoutWG.Done() })
- onlineNotifier := newMockOnlineNotifier(t)
- onlineNotifier.EXPECT().
- HostOnline(mock.Anything).
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{FoodGroup: wire.OService, SubGroup: wire.OServiceHostOnline},
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{},
- })
- buddyListRegistry := newMockBuddyListRegistry(t)
- buddyListRegistry.EXPECT().RegisterBuddyList(mock.Anything, mock.Anything).Return(nil)
- buddyListRegistry.EXPECT().UnregisterBuddyList(mock.Anything, mock.Anything).Return(nil)
- departureNotifier := newMockDepartureNotifier(t)
- departureNotifier.EXPECT().BroadcastBuddyDeparted(mock.Anything, mock.Anything).Return(nil)
- chatSessionManager := newMockChatSessionManager(t)
- chatSessionManager.EXPECT().RemoveUserFromAllChats(mock.Anything)
- server := oscarServer{
- authService: authService,
- buddyListRegistry: buddyListRegistry,
- chatSessionManager: chatSessionManager,
- departureNotifier: departureNotifier,
- onlineNotifier: onlineNotifier,
- logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
- recalcWarning: func(ctx context.Context, instance *state.SessionInstance) error { return nil },
- lowerWarnLevel: func(ctx context.Context, instance *state.SessionInstance) {},
- }
- // Fake client connection with address
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{Conn: serverConn, local: addr, remote: addr}
- // Coordinate when the server has finished login and sent HostOnline
- ready := make(chan struct{})
- // Client goroutine: perform handshake and then read forwarded messages
- go func() {
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- _ = wire.UnmarshalBE(&flap, clientConn)
- flapSignon := wire.FLAPSignonFrame{}
- _ = wire.UnmarshalBE(&flapSignon, bytes.NewBuffer(flap.Payload))
- // > send FLAPSignonFrame with login cookie
- flapSignon = wire.FLAPSignonFrame{FLAPVersion: 1}
- flapSignon.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")))
- buf := &bytes.Buffer{}
- _ = wire.MarshalBE(flapSignon, buf)
- _ = wire.MarshalBE(wire.FLAPFrame{StartMarker: 42, FrameType: wire.FLAPFrameSignon, Payload: buf.Bytes()}, clientConn)
- // Expect HostOnline
- flapcClient := wire.NewFlapClient(0, clientConn, clientConn)
- fr := wire.SNACFrame{}
- body := wire.SNAC_0x01_0x03_OServiceHostOnline{}
- _ = flapcClient.ReceiveSNAC(&fr, &body)
- close(ready)
- }()
- // Run the server handler in background so we can drive the session
- doneServer := make(chan error, 1)
- go func() { doneServer <- server.routeConnection(context.Background(), clientFake, config.Listener{}) }()
- // Wait for HostOnline to be received so session is ready
- select {
- case <-ready:
- case <-time.After(5 * time.Second):
- t.Fatal("server did not complete login in time")
- }
- // Now send messages via the session and verify client receives them
- messages := []wire.SNACMessage{
- {
- Frame: wire.SNACFrame{FoodGroup: wire.Buddy, SubGroup: wire.BuddyArrived},
- Body: wire.SNAC_0x03_0x0B_BuddyArrived{TLVUserInfo: wire.TLVUserInfo{ScreenName: "user1"}},
- },
- {
- Frame: wire.SNACFrame{FoodGroup: wire.Buddy, SubGroup: wire.BuddyDeparted},
- Body: wire.SNAC_0x03_0x0C_BuddyDeparted{TLVUserInfo: wire.TLVUserInfo{ScreenName: "user2"}},
- },
- {
- Frame: wire.SNACFrame{FoodGroup: wire.ICBM, SubGroup: 0x07},
- Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{Cookie: 12345, ChannelID: 1, TLVUserInfo: wire.TLVUserInfo{ScreenName: "sender"}},
- },
- }
- for i, msg := range messages {
- status := instance.RelayMessageToInstance(msg)
- assert.Equal(t, state.SessSendOK, status, "Message %d should be sent successfully", i)
- }
- // Read and verify all messages from client side
- for i, expected := range messages {
- flapFrame := wire.FLAPFrame{}
- err := wire.UnmarshalBE(&flapFrame, clientConn)
- assert.NoError(t, err, "read FLAP frame %d", i)
- assert.Equal(t, uint8(42), flapFrame.StartMarker)
- assert.Equal(t, wire.FLAPFrameData, flapFrame.FrameType)
- snac := wire.SNACFrame{}
- buf := bytes.NewBuffer(flapFrame.Payload)
- err = wire.UnmarshalBE(&snac, buf)
- assert.NoError(t, err, "unmarshal SNAC %d", i)
- assert.Equal(t, expected.Frame.FoodGroup, snac.FoodGroup)
- assert.Equal(t, expected.Frame.SubGroup, snac.SubGroup)
- }
- // CloseSession client to let server exit cleanly
- _ = clientConn.Close()
- // Wait for server handler to return
- select {
- case err := <-doneServer:
- assert.NoError(t, err)
- case <-time.After(5 * time.Second):
- t.Fatal("routeConnection did not exit in time")
- }
- // Ensure signout ran
- signoutWG.Wait()
- }
- func Test_oscarServer_receiveSessMessages_Chat_integration(t *testing.T) {
- serverConn, clientConn := net.Pipe()
- defer func() { _ = serverConn.Close() }()
- defer func() { _ = clientConn.Close() }()
- // Prepare session and mocks so we can exercise through routeConnection
- instance := state.NewSession().AddInstance()
- instance.SetSignonComplete()
- authService := newMockAuthService(t)
- authService.EXPECT().
- CrackCookie(mock.Anything).
- Return(state.ServerCookie{Service: wire.Chat}, nil)
- authService.EXPECT().
- RegisterChatSession(mock.Anything, state.ServerCookie{Service: wire.Chat}, mock.Anything).
- Run(func(_ context.Context, _ state.ServerCookie, cfg func(*state.Session)) {
- if cfg != nil {
- cfg(instance.Session())
- }
- }).
- Return(instance, nil)
- var signoutWG sync.WaitGroup
- signoutWG.Add(1)
- authService.EXPECT().
- SignoutChat(mock.Anything, instance.Session()).
- Run(func(ctx context.Context, s *state.Session) { signoutWG.Done() })
- onlineNotifier := newMockOnlineNotifier(t)
- onlineNotifier.EXPECT().
- HostOnline(mock.Anything).
- Return(wire.SNACMessage{
- Frame: wire.SNACFrame{FoodGroup: wire.OService, SubGroup: wire.OServiceHostOnline},
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{},
- })
- server := oscarServer{
- authService: authService,
- onlineNotifier: onlineNotifier,
- logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
- }
- // Fake client connection with address
- addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
- assert.NoError(t, err)
- clientFake := fakeConn{Conn: serverConn, local: addr, remote: addr}
- ready := make(chan struct{})
- // Client goroutine: perform handshake and then read forwarded messages
- go func() {
- // < receive FLAPSignonFrame
- flap := wire.FLAPFrame{}
- _ = wire.UnmarshalBE(&flap, clientConn)
- flapSignon := wire.FLAPSignonFrame{}
- _ = wire.UnmarshalBE(&flapSignon, bytes.NewBuffer(flap.Payload))
- // > send FLAPSignonFrame with login cookie
- flapSignon = wire.FLAPSignonFrame{FLAPVersion: 1}
- flapSignon.Append(wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")))
- buf := &bytes.Buffer{}
- _ = wire.MarshalBE(flapSignon, buf)
- _ = wire.MarshalBE(wire.FLAPFrame{StartMarker: 42, FrameType: wire.FLAPFrameSignon, Payload: buf.Bytes()}, clientConn)
- // Expect HostOnline
- flapcClient := wire.NewFlapClient(0, clientConn, clientConn)
- fr := wire.SNACFrame{}
- body := wire.SNAC_0x01_0x03_OServiceHostOnline{}
- _ = flapcClient.ReceiveSNAC(&fr, &body)
- close(ready)
- }()
- // Run the server handler in background so we can drive the session
- doneServer := make(chan error, 1)
- go func() { doneServer <- server.routeConnection(context.Background(), clientFake, config.Listener{}) }()
- // Wait for HostOnline to be received so session is ready
- select {
- case <-ready:
- case <-time.After(5 * time.Second):
- t.Fatal("server did not complete login in time")
- }
- messages := []wire.SNACMessage{
- {
- Frame: wire.SNACFrame{FoodGroup: wire.Chat, SubGroup: wire.ChatUsersJoined},
- Body: wire.SNAC_0x0E_0x03_ChatUsersJoined{Users: []wire.TLVUserInfo{{ScreenName: "user1"}}},
- },
- {
- Frame: wire.SNACFrame{FoodGroup: wire.Chat, SubGroup: wire.ChatUsersLeft},
- Body: wire.SNAC_0x0E_0x04_ChatUsersLeft{Users: []wire.TLVUserInfo{{ScreenName: "user2"}}},
- },
- {
- Frame: wire.SNACFrame{FoodGroup: wire.Chat, SubGroup: wire.ChatChannelMsgToClient},
- Body: wire.SNAC_0x0E_0x06_ChatChannelMsgToClient{
- Cookie: 12345, Channel: 1,
- TLVRestBlock: wire.TLVRestBlock{TLVList: wire.TLVList{
- wire.NewTLVBE(wire.ChatTLVSenderInformation, wire.TLVUserInfo{ScreenName: "sender"}),
- wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{TLVList: wire.TLVList{
- wire.NewTLVBE(wire.ChatTLVMessageInfoText, "Hello chat!"),
- }}),
- }},
- },
- },
- }
- for i, msg := range messages {
- status := instance.RelayMessageToInstance(msg)
- assert.Equal(t, state.SessSendOK, status, "Message %d should be sent successfully", i)
- }
- for i, expected := range messages {
- flapFrame := wire.FLAPFrame{}
- err := wire.UnmarshalBE(&flapFrame, clientConn)
- assert.NoError(t, err, "read FLAP frame %d", i)
- assert.Equal(t, uint8(42), flapFrame.StartMarker)
- assert.Equal(t, wire.FLAPFrameData, flapFrame.FrameType)
- snac := wire.SNACFrame{}
- buf := bytes.NewBuffer(flapFrame.Payload)
- err = wire.UnmarshalBE(&snac, buf)
- assert.NoError(t, err, "unmarshal SNAC %d", i)
- assert.Equal(t, expected.Frame.FoodGroup, snac.FoodGroup)
- assert.Equal(t, expected.Frame.SubGroup, snac.SubGroup)
- }
- _ = clientConn.Close()
- select {
- case err := <-doneServer:
- assert.NoError(t, err)
- case <-time.After(5 * time.Second):
- t.Fatal("routeConnection did not exit in time")
- }
- signoutWG.Wait()
- }
|