Skip to content

Commit 20ba37e

Browse files
committed
implement STATS foodgroup
Implements a no-op stats foodgroup handler that fixes AIM 1.0 crash that happens when the server shuts down and client attempts to send a stats SNAC.
1 parent a421dca commit 20ba37e

File tree

13 files changed

+295
-4
lines changed

13 files changed

+295
-4
lines changed

.mockery.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ packages:
102102
PermitDenyService:
103103
config:
104104
filename: "mock_permit_deny_service_test.go"
105+
StatsService:
106+
config:
107+
filename: "mock_stats_service_test.go"
105108
UserLookupService:
106109
config:
107110
filename: "mock_user_lookup_service_test.go"

cmd/server/factory.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ func BOS(deps Container) oscar.BOSServer {
305305
deps.snacRateLimits,
306306
)
307307
userLookupService := foodgroup.NewUserLookupService(deps.sqLiteUserStore)
308+
statsService := foodgroup.NewStatsService()
308309

309310
return oscar.BOSServer{
310311
AuthService: authService,
@@ -323,6 +324,7 @@ func BOS(deps Container) oscar.BOSServer {
323324
LocateHandler: handler.NewLocateHandler(locateService, logger),
324325
OServiceHandler: handler.NewOServiceHandler(logger, oServiceService),
325326
PermitDenyHandler: handler.NewPermitDenyHandler(logger, permitDenyService),
327+
StatsHandler: handler.NewStatsHandler(logger, statsService),
326328
UserLookupHandler: handler.NewUserLookupHandler(logger, userLookupService),
327329
}),
328330
Logger: logger,

foodgroup/oservice.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,18 @@ func (s OServiceServiceForBOS) ClientOnline(ctx context.Context, _ wire.SNAC_0x0
657657
return fmt.Errorf("unable to send buddy arrival notification: %w", err)
658658
}
659659

660+
msg := wire.SNACMessage{
661+
Frame: wire.SNACFrame{
662+
FoodGroup: wire.Stats,
663+
SubGroup: wire.StatsSetMinReportInterval,
664+
RequestID: wire.ReqIDFromServer,
665+
},
666+
Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
667+
MinReportInterval: 1,
668+
},
669+
}
670+
s.messageRelayer.RelayToScreenName(ctx, sess.IdentScreenName(), msg)
671+
660672
return nil
661673
}
662674

foodgroup/oservice_test.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,23 @@ func TestOServiceServiceForBOS_ClientOnline(t *testing.T) {
13111311
},
13121312
},
13131313
},
1314+
messageRelayerParams: messageRelayerParams{
1315+
relayToScreenNameParams: relayToScreenNameParams{
1316+
{
1317+
screenName: state.NewIdentScreenName("me"),
1318+
message: wire.SNACMessage{
1319+
Frame: wire.SNACFrame{
1320+
FoodGroup: wire.Stats,
1321+
SubGroup: wire.StatsSetMinReportInterval,
1322+
RequestID: wire.ReqIDFromServer,
1323+
},
1324+
Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
1325+
MinReportInterval: 1,
1326+
},
1327+
},
1328+
},
1329+
},
1330+
},
13141331
},
13151332
wantSess: newTestSession("me", sessOptCannedSignonTime, sessOptSignonComplete),
13161333
},
@@ -1320,13 +1337,18 @@ func TestOServiceServiceForBOS_ClientOnline(t *testing.T) {
13201337
buddyUpdateBroadcaster := newMockbuddyBroadcaster(t)
13211338
for _, params := range tt.mockParams.broadcastVisibilityParams {
13221339
buddyUpdateBroadcaster.EXPECT().
1323-
BroadcastVisibility(mock.Anything, matchSession(params.from), params.filter, params.doSendDepartures).
1340+
BroadcastVisibility(matchContext(), matchSession(params.from), params.filter, params.doSendDepartures).
13241341
Return(params.err)
13251342
}
1343+
messageRelayer := newMockMessageRelayer(t)
1344+
for _, params := range tt.mockParams.relayToScreenNameParams {
1345+
messageRelayer.EXPECT().
1346+
RelayToScreenName(matchContext(), params.screenName, params.message)
1347+
}
13261348

1327-
svc := NewOServiceServiceForBOS(config.Config{}, nil, slog.Default(), nil, nil, nil, nil, nil, wire.DefaultRateLimitClasses(), wire.DefaultSNACRateLimits())
1349+
svc := NewOServiceServiceForBOS(config.Config{}, messageRelayer, slog.Default(), nil, nil, nil, nil, nil, wire.DefaultRateLimitClasses(), wire.DefaultSNACRateLimits())
13281350
svc.buddyBroadcaster = buddyUpdateBroadcaster
1329-
haveErr := svc.ClientOnline(nil, tt.bodyIn, tt.sess)
1351+
haveErr := svc.ClientOnline(context.Background(), tt.bodyIn, tt.sess)
13301352
assert.ErrorIs(t, tt.wantErr, haveErr)
13311353
assert.Equal(t, tt.wantSess.SignonComplete(), tt.sess.SignonComplete())
13321354
})

foodgroup/stats.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package foodgroup
2+
3+
import (
4+
"context"
5+
6+
"github.com/mk6i/retro-aim-server/wire"
7+
)
8+
9+
func NewStatsService() StatsService {
10+
return StatsService{}
11+
}
12+
13+
type StatsService struct {
14+
}
15+
16+
// ReportEvents handles incoming stats events by acknowledging them without
17+
// processing. This is a no-op implementation to satisfy the client's
18+
// expectation of a response.
19+
func (s StatsService) ReportEvents(ctx context.Context, inFrame wire.SNACFrame, _ wire.SNAC_0x0B_0x03_StatsReportEvents) wire.SNACMessage {
20+
return wire.SNACMessage{
21+
Frame: wire.SNACFrame{
22+
FoodGroup: wire.Stats,
23+
SubGroup: wire.StatsReportAck,
24+
RequestID: inFrame.RequestID,
25+
},
26+
Body: wire.SNAC_0x0B_0x04_StatsReportAck{},
27+
}
28+
}

foodgroup/stats_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package foodgroup
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/mk6i/retro-aim-server/wire"
10+
)
11+
12+
func TestStatsService_ReportEvents(t *testing.T) {
13+
svc := NewStatsService()
14+
15+
frame := wire.SNACFrame{
16+
RequestID: 1234,
17+
}
18+
body := wire.SNAC_0x0B_0x03_StatsReportEvents{}
19+
20+
have := svc.ReportEvents(context.Background(), frame, body)
21+
22+
want := wire.SNACMessage{
23+
Frame: wire.SNACFrame{
24+
FoodGroup: wire.Stats,
25+
SubGroup: wire.StatsReportAck,
26+
RequestID: 1234,
27+
},
28+
Body: wire.SNAC_0x0B_0x04_StatsReportAck{},
29+
}
30+
31+
assert.Equal(t, want, have)
32+
}

server/oscar/handler/chat.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"log/slog"
77

88
"github.com/mk6i/retro-aim-server/server/oscar"
9-
109
"github.com/mk6i/retro-aim-server/server/oscar/middleware"
1110
"github.com/mk6i/retro-aim-server/state"
1211
"github.com/mk6i/retro-aim-server/wire"

server/oscar/handler/mock_stats_service_test.go

Lines changed: 85 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/oscar/handler/routes.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Handlers struct {
2323
ODirHandler
2424
OServiceHandler
2525
PermitDenyHandler
26+
StatsHandler
2627
UserLookupHandler
2728
}
2829

@@ -92,6 +93,8 @@ func NewBOSRouter(h Handlers) oscar.Router {
9293
router.Register(wire.PermitDeny, wire.PermitDenyRightsQuery, h.PermitDenyHandler.RightsQuery)
9394
router.Register(wire.PermitDeny, wire.PermitDenySetGroupPermitMask, h.PermitDenyHandler.SetGroupPermitMask)
9495

96+
router.Register(wire.Stats, wire.StatsReportEvents, h.StatsHandler.ReportEvents)
97+
9598
router.Register(wire.UserLookup, wire.UserLookupFindByEmail, h.UserLookupHandler.FindByEmail)
9699

97100
return router

server/oscar/handler/stats.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"io"
6+
"log/slog"
7+
8+
"github.com/mk6i/retro-aim-server/server/oscar"
9+
"github.com/mk6i/retro-aim-server/server/oscar/middleware"
10+
"github.com/mk6i/retro-aim-server/state"
11+
"github.com/mk6i/retro-aim-server/wire"
12+
)
13+
14+
type StatsService interface {
15+
ReportEvents(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0B_0x03_StatsReportEvents) wire.SNACMessage
16+
}
17+
18+
func NewStatsHandler(logger *slog.Logger, statsService StatsService) StatsHandler {
19+
return StatsHandler{
20+
StatsService: statsService,
21+
RouteLogger: middleware.RouteLogger{
22+
Logger: logger,
23+
},
24+
}
25+
}
26+
27+
type StatsHandler struct {
28+
StatsService
29+
middleware.RouteLogger
30+
}
31+
32+
func (h StatsHandler) ReportEvents(ctx context.Context, _ *state.Session, inFrame wire.SNACFrame, r io.Reader, rw oscar.ResponseWriter) error {
33+
inBody := wire.SNAC_0x0B_0x03_StatsReportEvents{}
34+
if err := wire.UnmarshalBE(&inBody, r); err != nil {
35+
return err
36+
}
37+
38+
outSNAC := h.StatsService.ReportEvents(ctx, inFrame, inBody)
39+
h.LogRequestAndResponse(ctx, inFrame, inBody, outSNAC.Frame, outSNAC.Body)
40+
41+
return rw.SendSNAC(outSNAC.Frame, outSNAC.Body)
42+
}

server/oscar/handler/stats_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package handler
2+
3+
import (
4+
"bytes"
5+
"log/slog"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/mock"
10+
11+
"github.com/mk6i/retro-aim-server/wire"
12+
)
13+
14+
func TestStatsHandler_ReportEvents(t *testing.T) {
15+
input := wire.SNACMessage{
16+
Frame: wire.SNACFrame{
17+
FoodGroup: wire.Stats,
18+
SubGroup: wire.StatsReportEvents,
19+
},
20+
Body: wire.SNAC_0x0B_0x03_StatsReportEvents{},
21+
}
22+
output := wire.SNACMessage{
23+
Frame: wire.SNACFrame{
24+
FoodGroup: wire.Stats,
25+
SubGroup: wire.StatsReportAck,
26+
},
27+
Body: wire.SNAC_0x0B_0x04_StatsReportAck{},
28+
}
29+
30+
svc := newMockStatsService(t)
31+
svc.EXPECT().
32+
ReportEvents(mock.Anything, input.Frame, input.Body).
33+
Return(output)
34+
35+
h := NewStatsHandler(slog.Default(), svc)
36+
37+
ss := newMockResponseWriter(t)
38+
ss.EXPECT().
39+
SendSNAC(output.Frame, output.Body).
40+
Return(nil)
41+
42+
buf := &bytes.Buffer{}
43+
assert.NoError(t, wire.MarshalBE(input.Body, buf))
44+
45+
assert.NoError(t, h.ReportEvents(nil, nil, input.Frame, buf, ss))
46+
}

wire/snacs.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,17 @@ const (
10291029
StatsReportAck uint16 = 0x0004
10301030
)
10311031

1032+
type SNAC_0x0B_0x02_StatsSetMinReportInterval struct {
1033+
MinReportInterval uint16
1034+
}
1035+
1036+
type SNAC_0x0B_0x03_StatsReportEvents struct {
1037+
TLVRestBlock
1038+
}
1039+
1040+
type SNAC_0x0B_0x04_StatsReportAck struct {
1041+
}
1042+
10321043
//
10331044
// 0x0C: Translate
10341045
//

wire/snacs_string.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,12 @@ var subGroupName = map[uint16]map[uint16]string{
299299
ODirKeywordListQuery: "ODirKeywordListQuery",
300300
ODirKeywordListReply: "ODirKeywordListReply",
301301
},
302+
Stats: {
303+
StatsErr: "StatsErr",
304+
StatsSetMinReportInterval: "StatsSetMinReportInterval",
305+
StatsReportEvents: "StatsReportEvents",
306+
StatsReportAck: "StatsReportAck",
307+
},
302308
}
303309

304310
// SubGroupName gets the string name of a subgroup within a food group. It

0 commit comments

Comments
 (0)