| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210 |
- 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,
- }
- 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}, 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,
- }
- 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}, 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, 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, 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_ConnectionProbe(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() {
- // < receive FLAPSignonFrame from server
- flap := wire.FLAPFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flap, clientConn))
- flapSignonFrame := wire.FLAPSignonFrame{}
- assert.NoError(t, wire.UnmarshalBE(&flapSignonFrame, bytes.NewBuffer(flap.Payload)))
- // Immediately close connection without sending anything back
- // This simulates connection probes that some ICQ clients do
- _ = clientConn.Close()
- }()
- rt := oscarServer{
- Logger: slog.Default(),
- IPRateLimiter: NewIPRateLimiter(rate.Every(1*time.Minute), 10, 1*time.Minute),
- }
- // routeConnection should return nil (not an error) for connection probes
- err = rt.routeConnection(context.Background(), clientFake, config.Listener{BOSAdvertisedHostPlain: "localhost:5190"})
- assert.NoError(t, err, "connection probe should not be treated as an error")
- }
- 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}).
- Return(instance, nil)
- wg.Add(1)
- authService.EXPECT().
- Signout(mock.Anything, instance).
- Run(func(ctx context.Context, s *state.SessionInstance) {
- 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}).
- 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}).
- 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}).
- Return(instance, nil)
- wg.Add(1)
- authService.EXPECT().
- SignoutChat(mock.Anything, instance).
- Run(func(ctx context.Context, s *state.SessionInstance) {
- 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 serverConn.Close()
- defer 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}).
- Return(instance, nil)
- var signoutWG sync.WaitGroup
- signoutWG.Add(1)
- authService.EXPECT().
- Signout(mock.Anything, instance).
- Run(func(ctx context.Context, s *state.SessionInstance) { 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 serverConn.Close()
- defer 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}).
- Return(instance, nil)
- var signoutWG sync.WaitGroup
- signoutWG.Add(1)
- authService.EXPECT().
- SignoutChat(mock.Anything, instance).
- Run(func(ctx context.Context, s *state.SessionInstance) { 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()
- }
|