瀏覽代碼

toc: implement toc_change_passwd

Mike 1 年之前
父節點
當前提交
901bed3cc0

+ 3 - 0
.mockery.yaml

@@ -160,6 +160,9 @@ packages:
           filename: "mock_user_manager_manager_test.go"
   github.com/mk6i/retro-aim-server/server/toc:
     interfaces:
+      AdminService:
+        config:
+          filename: "mock_admin_service_test.go"
       BuddyService:
         config:
           filename: "mock_buddy_service_test.go"

+ 7 - 0
cmd/server/factory.go

@@ -421,6 +421,13 @@ func TOC(deps Container) toc.Server {
 		Logger:     logger,
 		ListenAddr: net.JoinHostPort(deps.cfg.TOCHost, deps.cfg.TOCPort),
 		BOSProxy: toc.OSCARProxy{
+			AdminService: foodgroup.NewAdminService(
+				deps.sqLiteUserStore,
+				deps.sqLiteUserStore,
+				deps.inMemorySessionManager,
+				deps.inMemorySessionManager,
+				deps.logger,
+			),
 			AuthService: foodgroup.NewAuthService(
 				deps.cfg,
 				deps.inMemorySessionManager,

+ 52 - 0
server/toc/cmd_client.go

@@ -99,6 +99,7 @@ func (c *ChatRegistry) RetrieveSess(chatID int) *state.Session {
 //   - Receives incoming messages from the OSCAR server and translates them into
 //     TOC responses for the client.
 type OSCARProxy struct {
+	AdminService        AdminService
 	AuthService         AuthService
 	BuddyListRegistry   BuddyListRegistry
 	BuddyService        BuddyService
@@ -164,6 +165,8 @@ func (s OSCARProxy) RecvClientCmd(
 		return s.Evil(ctx, sessBOS, payload), true
 	case "toc_get_info":
 		return s.GetInfoURL(ctx, sessBOS, payload), true
+	case "toc_change_passwd":
+		return s.ChangePassword(ctx, sessBOS, payload), true
 	case "toc_chat_join", "toc_chat_accept":
 		var chatID int
 		var msg string
@@ -295,6 +298,55 @@ func (s OSCARProxy) AddDeny(ctx context.Context, me *state.Session, cmd []byte)
 	return ""
 }
 
+// ChangePassword handles the toc_change_passwd TOC command.
+//
+// From the TiK documentation:
+//
+//	Change a user's password. An ADMIN_PASSWD_STATUS or ERROR message will be
+//	sent back to the client.
+//
+// Command syntax: toc_change_passwd <existing_passwd> <new_passwd>
+func (s OSCARProxy) ChangePassword(ctx context.Context, me *state.Session, cmd []byte) string {
+	var oldPass, newPass string
+
+	if _, err := parseArgs(cmd, "toc_change_passwd", &oldPass, &newPass); err != nil {
+		return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
+	}
+
+	reqSNAC := wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+		TLVRestBlock: wire.TLVRestBlock{
+			TLVList: wire.TLVList{
+				wire.NewTLVBE(wire.AdminTLVOldPassword, oldPass),
+				wire.NewTLVBE(wire.AdminTLVNewPassword, newPass),
+			},
+		},
+	}
+
+	reply, err := s.AdminService.InfoChangeRequest(ctx, me, wire.SNACFrame{}, reqSNAC)
+	if err != nil {
+		return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: %w", err))
+	}
+
+	replyBody, ok := reply.Body.(wire.SNAC_0x07_0x05_AdminChangeReply)
+	if !ok {
+		return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", replyBody))
+	}
+
+	code, ok := replyBody.Uint16BE(wire.AdminTLVErrorCode)
+	if ok {
+		switch code {
+		case wire.AdminInfoErrorInvalidPasswordLength:
+			return "ERROR:911"
+		case wire.AdminInfoErrorValidatePassword:
+			return "ERROR:912"
+		default:
+			return "ERROR:913"
+		}
+	}
+
+	return "ADMIN_PASSWD_STATUS:0"
+}
+
 // ChatAccept handles the toc_chat_accept TOC command.
 //
 // From the TiK documentation:

+ 228 - 0
server/toc/cmd_client_test.go

@@ -282,6 +282,234 @@ func TestOSCARProxy_AddDeny(t *testing.T) {
 	}
 }
 
+func TestOSCARProxy_ChangePassword(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// me is the TOC user session
+		me *state.Session
+		// givenCmd is the TOC command
+		givenCmd []byte
+		// wantMsg is the expected TOC response
+		wantMsg string
+		// mockParams is the list of params sent to mocks that satisfy this
+		// method's dependencies
+		mockParams mockParams
+	}{
+		{
+			name:     "successfully change password",
+			me:       newTestSession("me"),
+			givenCmd: []byte("toc_change_passwd oldpass newpass"),
+			mockParams: mockParams{
+				adminParams: adminParams{
+					infoChangeRequestParams: infoChangeRequestParams{
+						{
+							me: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.AdminTLVOldPassword, "oldpass"),
+										wire.NewTLVBE(wire.AdminTLVNewPassword, "newpass"),
+									},
+								},
+							},
+							msg: wire.SNACMessage{
+								Body: wire.SNAC_0x07_0x05_AdminChangeReply{
+									Permissions: wire.AdminInfoPermissionsReadWrite,
+									TLVBlock: wire.TLVBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.AdminTLVNewPassword, []byte{}),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantMsg: "ADMIN_PASSWD_STATUS:0",
+		},
+		{
+			name:     "change password - invalid password length",
+			me:       newTestSession("me"),
+			givenCmd: []byte("toc_change_passwd oldpass np"),
+			mockParams: mockParams{
+				adminParams: adminParams{
+					infoChangeRequestParams: infoChangeRequestParams{
+						{
+							me: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.AdminTLVOldPassword, "oldpass"),
+										wire.NewTLVBE(wire.AdminTLVNewPassword, "np"),
+									},
+								},
+							},
+							msg: wire.SNACMessage{
+								Body: wire.SNAC_0x07_0x05_AdminChangeReply{
+									Permissions: wire.AdminInfoPermissionsReadWrite,
+									TLVBlock: wire.TLVBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.AdminTLVNewPassword, []byte{}),
+											wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidPasswordLength),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantMsg: "ERROR:911",
+		},
+		{
+			name:     "change password - incorrect password",
+			me:       newTestSession("me"),
+			givenCmd: []byte("toc_change_passwd oldpass baddpass"),
+			mockParams: mockParams{
+				adminParams: adminParams{
+					infoChangeRequestParams: infoChangeRequestParams{
+						{
+							me: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.AdminTLVOldPassword, "oldpass"),
+										wire.NewTLVBE(wire.AdminTLVNewPassword, "baddpass"),
+									},
+								},
+							},
+							msg: wire.SNACMessage{
+								Body: wire.SNAC_0x07_0x05_AdminChangeReply{
+									Permissions: wire.AdminInfoPermissionsReadWrite,
+									TLVBlock: wire.TLVBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.AdminTLVNewPassword, []byte{}),
+											wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorValidatePassword),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantMsg: "ERROR:912",
+		},
+		{
+			name:     "change password - catch-all error response",
+			me:       newTestSession("me"),
+			givenCmd: []byte("toc_change_passwd oldpass baddpass"),
+			mockParams: mockParams{
+				adminParams: adminParams{
+					infoChangeRequestParams: infoChangeRequestParams{
+						{
+							me: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.AdminTLVOldPassword, "oldpass"),
+										wire.NewTLVBE(wire.AdminTLVNewPassword, "baddpass"),
+									},
+								},
+							},
+							msg: wire.SNACMessage{
+								Body: wire.SNAC_0x07_0x05_AdminChangeReply{
+									Permissions: wire.AdminInfoPermissionsReadWrite,
+									TLVBlock: wire.TLVBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.AdminTLVNewPassword, []byte{}),
+											wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantMsg: "ERROR:913",
+		},
+		{
+			name:     "change password - runtime error from admin svc",
+			me:       newTestSession("me"),
+			givenCmd: []byte("toc_change_passwd oldpass baddpass"),
+			mockParams: mockParams{
+				adminParams: adminParams{
+					infoChangeRequestParams: infoChangeRequestParams{
+						{
+							me: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.AdminTLVOldPassword, "oldpass"),
+										wire.NewTLVBE(wire.AdminTLVNewPassword, "baddpass"),
+									},
+								},
+							},
+							err: io.EOF,
+						},
+					},
+				},
+			},
+			wantMsg: cmdInternalSvcErr,
+		},
+		{
+			name:     "change password - unexpected response from admin svc",
+			me:       newTestSession("me"),
+			givenCmd: []byte("toc_change_passwd oldpass baddpass"),
+			mockParams: mockParams{
+				adminParams: adminParams{
+					infoChangeRequestParams: infoChangeRequestParams{
+						{
+							me: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.AdminTLVOldPassword, "oldpass"),
+										wire.NewTLVBE(wire.AdminTLVNewPassword, "baddpass"),
+									},
+								},
+							},
+							msg: wire.SNACMessage{
+								Body: wire.SNACError{},
+							},
+						},
+					},
+				},
+			},
+			wantMsg: cmdInternalSvcErr,
+		},
+		{
+			name:     "bad command",
+			givenCmd: []byte(`toc_change_passwd`),
+			wantMsg:  cmdInternalSvcErr,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			adminSvc := newMockAdminService(t)
+			for _, params := range tc.mockParams.infoChangeRequestParams {
+				adminSvc.EXPECT().
+					InfoChangeRequest(ctx, matchSession(params.me), wire.SNACFrame{}, params.inBody).
+					Return(params.msg, params.err)
+			}
+
+			svc := OSCARProxy{
+				Logger:       slog.Default(),
+				AdminService: adminSvc,
+			}
+			msg := svc.ChangePassword(ctx, tc.me, tc.givenCmd)
+
+			assert.Equal(t, tc.wantMsg, msg)
+		})
+	}
+}
+
 func TestOSCARProxy_ChatAccept(t *testing.T) {
 	fnNewChatNavParams := func(err error) chatNavParams {
 		ret := chatNavParams{

+ 12 - 0
server/toc/helpers_test.go

@@ -7,6 +7,17 @@ import (
 	"github.com/mk6i/retro-aim-server/wire"
 )
 
+type adminParams struct {
+	infoChangeRequestParams
+}
+
+type infoChangeRequestParams []struct {
+	me     state.IdentScreenName
+	inBody wire.SNAC_0x07_0x04_AdminInfoChangeRequest
+	msg    wire.SNACMessage
+	err    error
+}
+
 type addBuddiesParams []struct {
 	me     state.IdentScreenName
 	inBody wire.SNAC_0x03_0x04_BuddyAddBuddies
@@ -232,6 +243,7 @@ type tocConfigParams struct {
 }
 
 type mockParams struct {
+	adminParams
 	authParams
 	buddyListRegistryParams
 	buddyParams

+ 98 - 0
server/toc/mock_admin_service_test.go

@@ -0,0 +1,98 @@
+// Code generated by mockery v2.52.1. DO NOT EDIT.
+
+package toc
+
+import (
+	context "context"
+
+	state "github.com/mk6i/retro-aim-server/state"
+	mock "github.com/stretchr/testify/mock"
+
+	wire "github.com/mk6i/retro-aim-server/wire"
+)
+
+// mockAdminService is an autogenerated mock type for the AdminService type
+type mockAdminService struct {
+	mock.Mock
+}
+
+type mockAdminService_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *mockAdminService) EXPECT() *mockAdminService_Expecter {
+	return &mockAdminService_Expecter{mock: &_m.Mock}
+}
+
+// InfoChangeRequest provides a mock function with given fields: ctx, sess, frame, body
+func (_m *mockAdminService) InfoChangeRequest(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error) {
+	ret := _m.Called(ctx, sess, frame, body)
+
+	if len(ret) == 0 {
+		panic("no return value specified for InfoChangeRequest")
+	}
+
+	var r0 wire.SNACMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error)); ok {
+		return rf(ctx, sess, frame, body)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) wire.SNACMessage); ok {
+		r0 = rf(ctx, sess, frame, body)
+	} else {
+		r0 = ret.Get(0).(wire.SNACMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) error); ok {
+		r1 = rf(ctx, sess, frame, body)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// mockAdminService_InfoChangeRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InfoChangeRequest'
+type mockAdminService_InfoChangeRequest_Call struct {
+	*mock.Call
+}
+
+// InfoChangeRequest is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *state.Session
+//   - frame wire.SNACFrame
+//   - body wire.SNAC_0x07_0x04_AdminInfoChangeRequest
+func (_e *mockAdminService_Expecter) InfoChangeRequest(ctx interface{}, sess interface{}, frame interface{}, body interface{}) *mockAdminService_InfoChangeRequest_Call {
+	return &mockAdminService_InfoChangeRequest_Call{Call: _e.mock.On("InfoChangeRequest", ctx, sess, frame, body)}
+}
+
+func (_c *mockAdminService_InfoChangeRequest_Call) Run(run func(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x04_AdminInfoChangeRequest)) *mockAdminService_InfoChangeRequest_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*state.Session), args[2].(wire.SNACFrame), args[3].(wire.SNAC_0x07_0x04_AdminInfoChangeRequest))
+	})
+	return _c
+}
+
+func (_c *mockAdminService_InfoChangeRequest_Call) Return(_a0 wire.SNACMessage, _a1 error) *mockAdminService_InfoChangeRequest_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *mockAdminService_InfoChangeRequest_Call) RunAndReturn(run func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error)) *mockAdminService_InfoChangeRequest_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// newMockAdminService creates a new instance of mockAdminService. 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 newMockAdminService(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *mockAdminService {
+	mock := &mockAdminService{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 4 - 0
server/toc/types.go

@@ -88,3 +88,7 @@ type CookieBaker interface {
 	Crack(data []byte) ([]byte, error)
 	Issue(data []byte) ([]byte, error)
 }
+
+type AdminService interface {
+	InfoChangeRequest(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error)
+}