Просмотр исходного кода

issue #14 - implement SNAC 09,03 PD RightsQuery

This commit implements the first handler in the Permit/Deny foodgroup,
RightsQuery, which allows Pidgin 2.12.0 login flow to successfully
complete.

This work reveals that the Permit/Deny food group needs to be
fully implemented in order to support user blocking in non-feedbag
AIM clients.

For now, we'll get PD out of the list of available foodgroups at login
until this is sorted out.
Mike 2 лет назад
Родитель
Сommit
11f5fd2582

+ 3 - 0
.mockery.yaml

@@ -66,6 +66,9 @@ packages:
       BARTService:
       BARTService:
         config:
         config:
           filename: "mock_bart_test.go"
           filename: "mock_bart_test.go"
+      PermitDenyService:
+        config:
+          filename: "mock_permit_deny_test.go"
   github.com/mk6i/retro-aim-server/foodgroup:
   github.com/mk6i/retro-aim-server/foodgroup:
     interfaces:
     interfaces:
       FeedbagManager:
       FeedbagManager:

+ 2 - 1
README.md

@@ -22,7 +22,8 @@ The following features are supported:
 - [x] Chat Rooms (v4.x, v5.x)
 - [x] Chat Rooms (v4.x, v5.x)
 - [x] Instant Messaging
 - [x] Instant Messaging
 - [x] User Profiles
 - [x] User Profiles
-- [x] Blocking / Visibility Toggle / Idle Notification
+- [x] Blocking (v3.5+)
+- [x] Visibility Toggle / Idle Notification
 - [x] Warning
 - [x] Warning
 
 
 ## 🏁 How to Run
 ## 🏁 How to Run

+ 2 - 0
cmd/server/main.go

@@ -55,6 +55,7 @@ func main() {
 		chatNavService := foodgroup.NewChatNavService(logger, chatRegistry, state.NewChatRoom, newChatSessMgr)
 		chatNavService := foodgroup.NewChatNavService(logger, chatRegistry, state.NewChatRoom, newChatSessMgr)
 		feedbagService := foodgroup.NewFeedbagService(logger, sessionManager, feedbagStore, feedbagStore, adjListBuddyListStore)
 		feedbagService := foodgroup.NewFeedbagService(logger, sessionManager, feedbagStore, feedbagStore, adjListBuddyListStore)
 		icbmService := foodgroup.NewICBMService(sessionManager, feedbagStore, adjListBuddyListStore)
 		icbmService := foodgroup.NewICBMService(sessionManager, feedbagStore, adjListBuddyListStore)
+		foodgroupService := foodgroup.NewPermitDenyService()
 
 
 		oscar.BOSServer{
 		oscar.BOSServer{
 			AuthService: authService,
 			AuthService: authService,
@@ -68,6 +69,7 @@ func main() {
 				ICBMHandler:        handler.NewICBMHandler(logger, icbmService),
 				ICBMHandler:        handler.NewICBMHandler(logger, icbmService),
 				LocateHandler:      handler.NewLocateHandler(locateService, logger),
 				LocateHandler:      handler.NewLocateHandler(locateService, logger),
 				OServiceBOSHandler: handler.NewOServiceHandlerForBOS(logger, oServiceService, oServiceServiceForBOS),
 				OServiceBOSHandler: handler.NewOServiceHandlerForBOS(logger, oServiceService, oServiceServiceForBOS),
+				PermitDenyHandler:  handler.NewPermitDenyHandler(logger, foodgroupService),
 			}),
 			}),
 			Logger:         logger,
 			Logger:         logger,
 			OnlineNotifier: oServiceServiceForBOS,
 			OnlineNotifier: oServiceServiceForBOS,

+ 42 - 0
foodgroup/permit_deny.go

@@ -0,0 +1,42 @@
+package foodgroup
+
+import (
+	"context"
+
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+// NewPermitDenyService creates an instance of PermitDenyService.
+func NewPermitDenyService() PermitDenyService {
+	return PermitDenyService{}
+}
+
+// PermitDenyService provides functionality for the PermitDeny (PD) food group.
+// The PD food group manages settings for permit/deny (allow/block) for
+// pre-feedbag (sever-side buddy list) AIM clients. Right now it's stubbed out
+// to support pidgin. Eventually this food group will be fully implemented in
+// order to support client blocking in AIM <= 3.0.
+type PermitDenyService struct {
+}
+
+// RightsQuery returns settings for the PermitDeny food group. It returns SNAC
+// wire.PermitDenyRightsReply. The values in the return SNAC were arbitrarily
+// chosen.
+func (s PermitDenyService) RightsQuery(_ context.Context, frame wire.SNACFrame) wire.SNACMessage {
+	return wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.PermitDeny,
+			SubGroup:  wire.PermitDenyRightsReply,
+			RequestID: frame.RequestID,
+		},
+		Body: wire.SNAC_0x09_0x03_PermitDenyRightsReply{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.PermitDenyTLVMaxDenies, uint16(100)),
+					wire.NewTLV(wire.PermitDenyTLVMaxPermits, uint16(100)),
+					wire.NewTLV(wire.PermitDenyTLVMaxTempPermits, uint16(100)),
+				},
+			},
+		},
+	}
+}

+ 33 - 0
foodgroup/permit_deny_test.go

@@ -0,0 +1,33 @@
+package foodgroup
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+func TestPermitDenyService_RightsQuery(t *testing.T) {
+	svc := NewPermitDenyService()
+
+	have := svc.RightsQuery(nil, wire.SNACFrame{RequestID: 1234})
+	want := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.PermitDeny,
+			SubGroup:  wire.PermitDenyRightsReply,
+			RequestID: 1234,
+		},
+		Body: wire.SNAC_0x09_0x03_PermitDenyRightsReply{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.PermitDenyTLVMaxDenies, uint16(100)),
+					wire.NewTLV(wire.PermitDenyTLVMaxPermits, uint16(100)),
+					wire.NewTLV(wire.PermitDenyTLVMaxTempPermits, uint16(100)),
+				},
+			},
+		},
+	}
+
+	assert.Equal(t, want, have)
+}

+ 84 - 0
server/oscar/handler/mock_permit_deny_test.go

@@ -0,0 +1,84 @@
+// Code generated by mockery v2.40.1. DO NOT EDIT.
+
+package handler
+
+import (
+	context "context"
+
+	wire "github.com/mk6i/retro-aim-server/wire"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// mockPermitDenyService is an autogenerated mock type for the PermitDenyService type
+type mockPermitDenyService struct {
+	mock.Mock
+}
+
+type mockPermitDenyService_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *mockPermitDenyService) EXPECT() *mockPermitDenyService_Expecter {
+	return &mockPermitDenyService_Expecter{mock: &_m.Mock}
+}
+
+// RightsQuery provides a mock function with given fields: _a0, frame
+func (_m *mockPermitDenyService) RightsQuery(_a0 context.Context, frame wire.SNACFrame) wire.SNACMessage {
+	ret := _m.Called(_a0, frame)
+
+	if len(ret) == 0 {
+		panic("no return value specified for RightsQuery")
+	}
+
+	var r0 wire.SNACMessage
+	if rf, ok := ret.Get(0).(func(context.Context, wire.SNACFrame) wire.SNACMessage); ok {
+		r0 = rf(_a0, frame)
+	} else {
+		r0 = ret.Get(0).(wire.SNACMessage)
+	}
+
+	return r0
+}
+
+// mockPermitDenyService_RightsQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RightsQuery'
+type mockPermitDenyService_RightsQuery_Call struct {
+	*mock.Call
+}
+
+// RightsQuery is a helper method to define mock.On call
+//   - _a0 context.Context
+//   - frame wire.SNACFrame
+func (_e *mockPermitDenyService_Expecter) RightsQuery(_a0 interface{}, frame interface{}) *mockPermitDenyService_RightsQuery_Call {
+	return &mockPermitDenyService_RightsQuery_Call{Call: _e.mock.On("RightsQuery", _a0, frame)}
+}
+
+func (_c *mockPermitDenyService_RightsQuery_Call) Run(run func(_a0 context.Context, frame wire.SNACFrame)) *mockPermitDenyService_RightsQuery_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(wire.SNACFrame))
+	})
+	return _c
+}
+
+func (_c *mockPermitDenyService_RightsQuery_Call) Return(_a0 wire.SNACMessage) *mockPermitDenyService_RightsQuery_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *mockPermitDenyService_RightsQuery_Call) RunAndReturn(run func(context.Context, wire.SNACFrame) wire.SNACMessage) *mockPermitDenyService_RightsQuery_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// newMockPermitDenyService creates a new instance of mockPermitDenyService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func newMockPermitDenyService(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *mockPermitDenyService {
+	mock := &mockPermitDenyService{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 36 - 0
server/oscar/handler/permit_deny.go

@@ -0,0 +1,36 @@
+package handler
+
+import (
+	"context"
+	"io"
+	"log/slog"
+
+	"github.com/mk6i/retro-aim-server/server/oscar"
+	"github.com/mk6i/retro-aim-server/server/oscar/middleware"
+	"github.com/mk6i/retro-aim-server/state"
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+type PermitDenyService interface {
+	RightsQuery(_ context.Context, frame wire.SNACFrame) wire.SNACMessage
+}
+
+func NewPermitDenyHandler(logger *slog.Logger, permitDenyService PermitDenyService) PermitDenyHandler {
+	return PermitDenyHandler{
+		RouteLogger: middleware.RouteLogger{
+			Logger: logger,
+		},
+		PermitDenyService: permitDenyService,
+	}
+}
+
+type PermitDenyHandler struct {
+	PermitDenyService
+	middleware.RouteLogger
+}
+
+func (rt PermitDenyHandler) RightsQuery(ctx context.Context, _ *state.Session, inFrame wire.SNACFrame, _ io.Reader, rw oscar.ResponseWriter) error {
+	outSNAC := rt.PermitDenyService.RightsQuery(ctx, inFrame)
+	rt.LogRequestAndResponse(ctx, inFrame, nil, outSNAC.Frame, outSNAC.Body)
+	return rw.SendSNAC(outSNAC.Frame, outSNAC.Body)
+}

+ 52 - 0
server/oscar/handler/permit_deny_test.go

@@ -0,0 +1,52 @@
+package handler
+
+import (
+	"bytes"
+	"log/slog"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+func TestPermitDenyHandler_RightsQuery(t *testing.T) {
+	input := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.PermitDeny,
+			SubGroup:  wire.PermitDenyRightsQuery,
+		},
+		Body: struct{}{},
+	}
+	output := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.PermitDeny,
+			SubGroup:  wire.PermitDenyRightsReply,
+		},
+		Body: wire.SNAC_0x09_0x03_PermitDenyRightsReply{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(0x01, uint16(1000)),
+				},
+			},
+		},
+	}
+
+	svc := newMockPermitDenyService(t)
+	svc.EXPECT().
+		RightsQuery(mock.Anything, input.Frame).
+		Return(output)
+
+	h := NewPermitDenyHandler(slog.Default(), svc)
+
+	responseWriter := newMockResponseWriter(t)
+	responseWriter.EXPECT().
+		SendSNAC(output.Frame, output.Body).
+		Return(nil)
+
+	buf := &bytes.Buffer{}
+	assert.NoError(t, wire.Marshal(input.Body, buf))
+
+	assert.NoError(t, h.RightsQuery(nil, nil, input.Frame, buf, responseWriter))
+}

+ 3 - 0
server/oscar/handler/routes.go

@@ -22,6 +22,7 @@ type Handlers struct {
 	OServiceBOSHandler
 	OServiceBOSHandler
 	OServiceChatHandler
 	OServiceChatHandler
 	OServiceChatNavHandler
 	OServiceChatNavHandler
+	PermitDenyHandler
 }
 }
 
 
 // NewBOSRouter initializes and configures a new Router instance for handling
 // NewBOSRouter initializes and configures a new Router instance for handling
@@ -68,6 +69,8 @@ func NewBOSRouter(h Handlers) oscar.Router {
 	router.Register(wire.Locate, wire.LocateUserInfoQuery, h.LocateHandler.UserInfoQuery)
 	router.Register(wire.Locate, wire.LocateUserInfoQuery, h.LocateHandler.UserInfoQuery)
 	router.Register(wire.Locate, wire.LocateUserInfoQuery2, h.LocateHandler.UserInfoQuery2)
 	router.Register(wire.Locate, wire.LocateUserInfoQuery2, h.LocateHandler.UserInfoQuery2)
 
 
+	router.Register(wire.PermitDeny, wire.PermitDenyRightsQuery, h.PermitDenyHandler.RightsQuery)
+
 	router.Register(wire.OService, wire.OServiceClientOnline, h.OServiceBOSHandler.ClientOnline)
 	router.Register(wire.OService, wire.OServiceClientOnline, h.OServiceBOSHandler.ClientOnline)
 	router.Register(wire.OService, wire.OServiceClientVersions, h.OServiceBOSHandler.OServiceHandler.ClientVersions)
 	router.Register(wire.OService, wire.OServiceClientVersions, h.OServiceBOSHandler.OServiceHandler.ClientVersions)
 	router.Register(wire.OService, wire.OServiceIdleNotification, h.OServiceBOSHandler.OServiceHandler.IdleNotification)
 	router.Register(wire.OService, wire.OServiceIdleNotification, h.OServiceBOSHandler.OServiceHandler.IdleNotification)

+ 21 - 3
wire/snacs.go

@@ -22,7 +22,7 @@ const (
 	Invite      uint16 = 0x0006
 	Invite      uint16 = 0x0006
 	Admin       uint16 = 0x0007
 	Admin       uint16 = 0x0007
 	Popup       uint16 = 0x0008
 	Popup       uint16 = 0x0008
-	PD          uint16 = 0x0009
+	PermitDeny  uint16 = 0x0009
 	UserLookup  uint16 = 0x000A
 	UserLookup  uint16 = 0x000A
 	Stats       uint16 = 0x000B
 	Stats       uint16 = 0x000B
 	Translate   uint16 = 0x000C
 	Translate   uint16 = 0x000C
@@ -558,10 +558,28 @@ type SNAC_0x04_0x14_ICBMClientEvent struct {
 }
 }
 
 
 //
 //
-// 0x09: PD
+// 0x09: PermitDeny
 //
 //
 
 
-type SNAC_0x09_0x03_PDRightsReply struct {
+const (
+	PermitDenyErr                      uint16 = 0x0001
+	PermitDenyRightsQuery              uint16 = 0x0002
+	PermitDenyRightsReply              uint16 = 0x0003
+	PermitDenySetGroupPermitMask       uint16 = 0x0004
+	PermitDenyAddPermListEntries       uint16 = 0x0005
+	PermitDenyDelPermListEntries       uint16 = 0x0006
+	PermitDenyAddDenyListEntries       uint16 = 0x0007
+	PermitDenyDelDenyListEntries       uint16 = 0x0008
+	PermitDenyBosErr                   uint16 = 0x0009
+	PermitDenyAddTempPermitListEntries uint16 = 0x000A
+	PermitDenyDelTempPermitListEntries uint16 = 0x000B
+
+	PermitDenyTLVMaxPermits     uint16 = 0x01
+	PermitDenyTLVMaxDenies      uint16 = 0x02
+	PermitDenyTLVMaxTempPermits uint16 = 0x03
+)
+
+type SNAC_0x09_0x03_PermitDenyRightsReply struct {
 	TLVRestBlock
 	TLVRestBlock
 }
 }
 
 

+ 14 - 1
wire/snacs_string.go

@@ -9,7 +9,7 @@ var foodGroupName = map[uint16]string{
 	Invite:      "Invite",
 	Invite:      "Invite",
 	Admin:       "Admin",
 	Admin:       "Admin",
 	Popup:       "Popup",
 	Popup:       "Popup",
-	PD:          "PD",
+	PermitDeny:  "PermitDeny",
 	UserLookup:  "UserLookup",
 	UserLookup:  "UserLookup",
 	Stats:       "Stats",
 	Stats:       "Stats",
 	Translate:   "Translate",
 	Translate:   "Translate",
@@ -263,6 +263,19 @@ var subGroupName = map[uint16]map[uint16]string{
 		BARTDownload2Query: "BARTDownload2Query",
 		BARTDownload2Query: "BARTDownload2Query",
 		BARTDownload2Reply: "BARTDownload2Reply",
 		BARTDownload2Reply: "BARTDownload2Reply",
 	},
 	},
+	PermitDeny: {
+		PermitDenyErr:                      "PermitDenyErr",
+		PermitDenyRightsQuery:              "PermitDenyRightsQuery",
+		PermitDenyRightsReply:              "PermitDenyRightsReply",
+		PermitDenySetGroupPermitMask:       "PermitDenySetGroupPermitMask",
+		PermitDenyAddPermListEntries:       "PermitDenyAddPermListEntries",
+		PermitDenyDelPermListEntries:       "PermitDenyDelPermListEntries",
+		PermitDenyAddDenyListEntries:       "PermitDenyAddDenyListEntries",
+		PermitDenyDelDenyListEntries:       "PermitDenyDelDenyListEntries",
+		PermitDenyBosErr:                   "PermitDenyBosErr",
+		PermitDenyAddTempPermitListEntries: "PermitDenyAddTempPermitListEntries",
+		PermitDenyDelTempPermitListEntries: "PermitDenyDelTempPermitListEntries",
+	},
 }
 }
 
 
 // SubGroupName gets the string name of a subgroup within a food group. It
 // SubGroupName gets the string name of a subgroup within a food group. It