Mike пре 2 година
родитељ
комит
68319b67e6
14 измењених фајлова са 795 додато и 401 уклоњено
  1. 1 1
      .mockery.yaml
  2. 27 0
      oscar/snacs.go
  3. 233 0
      server/MockICBMHandler.go
  4. 1 1
      server/bucp.go
  5. 1 1
      server/buddy.go
  6. 1 1
      server/chat.go
  7. 1 1
      server/chat_nav.go
  8. 1 1
      server/feedbag.go
  9. 166 188
      server/icbm.go
  10. 343 199
      server/icbm_test.go
  11. 1 1
      server/locate.go
  12. 1 1
      server/oservice.go
  13. 1 1
      server/pd.go
  14. 17 5
      server/protocol.go

+ 1 - 1
.mockery.yaml

@@ -9,4 +9,4 @@ packages:
       FeedbagManager:
       SessionManager:
       ProfileManager:
-# Lots more config...
+      ICBMHandler:

+ 27 - 0
oscar/snacs.go

@@ -169,6 +169,33 @@ type SNAC_0x03_0x0B_BuddyDeparted struct {
 // 0x04: ICBM
 //
 
+const (
+	ICBMErr                uint16 = 0x0001
+	ICBMAddParameters      uint16 = 0x0002
+	ICBMDelParameters      uint16 = 0x0003
+	ICBMParameterQuery     uint16 = 0x0004
+	ICBMParameterReply     uint16 = 0x0005
+	ICBMChannelMsgToHost   uint16 = 0x0006
+	ICBMChannelMsgToclient uint16 = 0x0007
+	ICBMEvilRequest        uint16 = 0x0008
+	ICBMEvilReply          uint16 = 0x0009
+	ICBMMissedCalls        uint16 = 0x000A
+	ICBMClientErr          uint16 = 0x000B
+	ICBMHostAck            uint16 = 0x000C
+	ICBMSinStored          uint16 = 0x000D
+	ICBMSinListQuery       uint16 = 0x000E
+	ICBMSinListReply       uint16 = 0x000F
+	ICBMSinRetrieve        uint16 = 0x0010
+	ICBMSinDelete          uint16 = 0x0011
+	ICBMNotifyRequest      uint16 = 0x0012
+	ICBMNotifyReply        uint16 = 0x0013
+	ICBMClientEvent        uint16 = 0x0014
+	ICBMSinReply           uint16 = 0x0017
+
+	ICBMTLVTagRequestHostAck uint16 = 0x03
+	ICBMTLVTagsWantEvents    uint16 = 0x0B
+)
+
 type SNAC_0x04_0x02_ICBMAddParameters struct {
 	Channel              uint16
 	ICBMFlags            uint32

+ 233 - 0
server/MockICBMHandler.go

@@ -0,0 +1,233 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import (
+	oscar "github.com/mkaminski/goaim/oscar"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// MockICBMHandler is an autogenerated mock type for the ICBMHandler type
+type MockICBMHandler struct {
+	mock.Mock
+}
+
+type MockICBMHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockICBMHandler) EXPECT() *MockICBMHandler_Expecter {
+	return &MockICBMHandler_Expecter{mock: &_m.Mock}
+}
+
+// ChannelMsgToHostHandler provides a mock function with given fields: sm, fm, sess, snacPayloadIn
+func (_m *MockICBMHandler) ChannelMsgToHostHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error) {
+	ret := _m.Called(sm, fm, sess, snacPayloadIn)
+
+	var r0 *XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)); ok {
+		return rf(sm, fm, sess, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) *XMessage); ok {
+		r0 = rf(sm, fm, sess, snacPayloadIn)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*XMessage)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) error); ok {
+		r1 = rf(sm, fm, sess, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockICBMHandler_ChannelMsgToHostHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChannelMsgToHostHandler'
+type MockICBMHandler_ChannelMsgToHostHandler_Call struct {
+	*mock.Call
+}
+
+// ChannelMsgToHostHandler is a helper method to define mock.On call
+//   - sm SessionManager
+//   - fm FeedbagManager
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost
+func (_e *MockICBMHandler_Expecter) ChannelMsgToHostHandler(sm interface{}, fm interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_ChannelMsgToHostHandler_Call {
+	return &MockICBMHandler_ChannelMsgToHostHandler_Call{Call: _e.mock.On("ChannelMsgToHostHandler", sm, fm, sess, snacPayloadIn)}
+}
+
+func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) Run(run func(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost)) *MockICBMHandler_ChannelMsgToHostHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(SessionManager), args[1].(FeedbagManager), args[2].(*Session), args[3].(oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost))
+	})
+	return _c
+}
+
+func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) Return(_a0 *XMessage, _a1 error) *MockICBMHandler_ChannelMsgToHostHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)) *MockICBMHandler_ChannelMsgToHostHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ClientEventHandler provides a mock function with given fields: sm, fm, sess, snacPayloadIn
+func (_m *MockICBMHandler) ClientEventHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error {
+	ret := _m.Called(sm, fm, sess, snacPayloadIn)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x14_ICBMClientEvent) error); ok {
+		r0 = rf(sm, fm, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockICBMHandler_ClientEventHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientEventHandler'
+type MockICBMHandler_ClientEventHandler_Call struct {
+	*mock.Call
+}
+
+// ClientEventHandler is a helper method to define mock.On call
+//   - sm SessionManager
+//   - fm FeedbagManager
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent
+func (_e *MockICBMHandler_Expecter) ClientEventHandler(sm interface{}, fm interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_ClientEventHandler_Call {
+	return &MockICBMHandler_ClientEventHandler_Call{Call: _e.mock.On("ClientEventHandler", sm, fm, sess, snacPayloadIn)}
+}
+
+func (_c *MockICBMHandler_ClientEventHandler_Call) Run(run func(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent)) *MockICBMHandler_ClientEventHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(SessionManager), args[1].(FeedbagManager), args[2].(*Session), args[3].(oscar.SNAC_0x04_0x14_ICBMClientEvent))
+	})
+	return _c
+}
+
+func (_c *MockICBMHandler_ClientEventHandler_Call) Return(_a0 error) *MockICBMHandler_ClientEventHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockICBMHandler_ClientEventHandler_Call) RunAndReturn(run func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x14_ICBMClientEvent) error) *MockICBMHandler_ClientEventHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// EvilRequestHandler provides a mock function with given fields: sm, fm, sess, snacPayloadIn
+func (_m *MockICBMHandler) EvilRequestHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error) {
+	ret := _m.Called(sm, fm, sess, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)); ok {
+		return rf(sm, fm, sess, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) XMessage); ok {
+		r0 = rf(sm, fm, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) error); ok {
+		r1 = rf(sm, fm, sess, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockICBMHandler_EvilRequestHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvilRequestHandler'
+type MockICBMHandler_EvilRequestHandler_Call struct {
+	*mock.Call
+}
+
+// EvilRequestHandler is a helper method to define mock.On call
+//   - sm SessionManager
+//   - fm FeedbagManager
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest
+func (_e *MockICBMHandler_Expecter) EvilRequestHandler(sm interface{}, fm interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_EvilRequestHandler_Call {
+	return &MockICBMHandler_EvilRequestHandler_Call{Call: _e.mock.On("EvilRequestHandler", sm, fm, sess, snacPayloadIn)}
+}
+
+func (_c *MockICBMHandler_EvilRequestHandler_Call) Run(run func(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest)) *MockICBMHandler_EvilRequestHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(SessionManager), args[1].(FeedbagManager), args[2].(*Session), args[3].(oscar.SNAC_0x04_0x08_ICBMEvilRequest))
+	})
+	return _c
+}
+
+func (_c *MockICBMHandler_EvilRequestHandler_Call) Return(_a0 XMessage, _a1 error) *MockICBMHandler_EvilRequestHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockICBMHandler_EvilRequestHandler_Call) RunAndReturn(run func(SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)) *MockICBMHandler_EvilRequestHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ParameterQueryHandler provides a mock function with given fields:
+func (_m *MockICBMHandler) ParameterQueryHandler() XMessage {
+	ret := _m.Called()
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func() XMessage); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockICBMHandler_ParameterQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ParameterQueryHandler'
+type MockICBMHandler_ParameterQueryHandler_Call struct {
+	*mock.Call
+}
+
+// ParameterQueryHandler is a helper method to define mock.On call
+func (_e *MockICBMHandler_Expecter) ParameterQueryHandler() *MockICBMHandler_ParameterQueryHandler_Call {
+	return &MockICBMHandler_ParameterQueryHandler_Call{Call: _e.mock.On("ParameterQueryHandler")}
+}
+
+func (_c *MockICBMHandler_ParameterQueryHandler_Call) Run(run func()) *MockICBMHandler_ParameterQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockICBMHandler_ParameterQueryHandler_Call) Return(_a0 XMessage) *MockICBMHandler_ParameterQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockICBMHandler_ParameterQueryHandler_Call) RunAndReturn(run func() XMessage) *MockICBMHandler_ParameterQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockICBMHandler creates a new instance of MockICBMHandler. 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 NewMockICBMHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockICBMHandler {
+	mock := &MockICBMHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 1 - 1
server/bucp.go

@@ -22,7 +22,7 @@ const (
 )
 
 func routeBUCP() error {
-	return ErrUnimplementedSNAC
+	return ErrUnsupportedSubGroup
 }
 
 func ReceiveAndSendAuthChallenge(cfg Config, fm *FeedbagStore, r io.Reader, w io.Writer, sequence *uint32, newUUID func() uuid.UUID) error {

+ 1 - 1
server/buddy.go

@@ -27,7 +27,7 @@ func routeBuddy(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32
 	case BuddyRightsQuery:
 		return SendAndReceiveBuddyRights(snac, r, w, sequence)
 	default:
-		return ErrUnimplementedSNAC
+		return ErrUnsupportedSubGroup
 	}
 }
 

+ 1 - 1
server/chat.go

@@ -55,7 +55,7 @@ func routeChat(sess *Session, sm SessionManager, snac oscar.SnacFrame, r io.Read
 	case ChatChannelMsgTohost:
 		return SendAndReceiveChatChannelMsgTohost(sess, sm, snac, r, w, sequence)
 	default:
-		return ErrUnimplementedSNAC
+		return ErrUnsupportedSubGroup
 	}
 }
 

+ 1 - 1
server/chat_nav.go

@@ -30,7 +30,7 @@ func routeChatNav(sess *Session, cr *ChatRegistry, snac oscar.SnacFrame, r io.Re
 	case ChatNavCreateRoom:
 		return SendAndReceiveCreateRoom(sess, cr, NewChatRoom, snac, r, w, sequence)
 	default:
-		return ErrUnimplementedSNAC
+		return ErrUnsupportedSubGroup
 	}
 }
 

+ 1 - 1
server/feedbag.go

@@ -167,7 +167,7 @@ func routeFeedbag(sm SessionManager, sess *Session, fm *FeedbagStore, snac oscar
 	case FeedbagEndCluster:
 		return ReceiveFeedbagEndCluster(snac, r, w, sequence)
 	default:
-		return ErrUnimplementedSNAC
+		return ErrUnsupportedSubGroup
 	}
 }
 

+ 166 - 188
server/icbm.go

@@ -2,117 +2,136 @@ package server
 
 import (
 	"errors"
-	"fmt"
-	"github.com/mkaminski/goaim/oscar"
 	"io"
-)
 
-const (
-	ICBMErr                uint16 = 0x0001
-	ICBMAddParameters             = 0x0002
-	ICBMDelParameters             = 0x0003
-	ICBMParameterQuery            = 0x0004
-	ICBMParameterReply            = 0x0005
-	ICBMChannelMsgTohost          = 0x0006
-	ICBMChannelMsgToclient        = 0x0007
-	ICBMEvilRequest               = 0x0008
-	ICBMEvilReply                 = 0x0009
-	ICBMMissedCalls               = 0x000A
-	ICBMClientErr                 = 0x000B
-	ICBMHostAck                   = 0x000C
-	ICBMSinStored                 = 0x000D
-	ICBMSinListQuery              = 0x000E
-	ICBMSinListReply              = 0x000F
-	ICBMSinRetrieve               = 0x0010
-	ICBMSinDelete                 = 0x0011
-	ICBMNotifyRequest             = 0x0012
-	ICBMNotifyReply               = 0x0013
-	ICBMClientEvent               = 0x0014
-	ICBMSinReply                  = 0x0017
+	"github.com/mkaminski/goaim/oscar"
 )
 
 const (
-	ICBMTLVTagRequestHostAck uint16 = 0x03
-	ICBMTLVTagsWantEvents    uint16 = 0x0B
+	evilDelta     = uint16(100)
+	evilDeltaAnon = uint16(30)
 )
 
-func routeICBM(sm SessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	switch snac.SubGroup {
-	case ICBMAddParameters:
-		return ReceiveAddParameters(snac, r)
-	case ICBMParameterQuery:
-		return SendAndReceiveICBMParameterReply(snac, r, w, sequence)
-	case ICBMChannelMsgTohost:
-		return SendAndReceiveChannelMsgTohost(sm, fm, sess, snac, r, w, sequence)
-	case ICBMEvilRequest:
-		return SendAndReceiveEvilRequest(sm, fm, sess, snac, r, w, sequence)
-	case ICBMClientErr:
-		return ReceiveClientErr(snac, r)
-	case ICBMClientEvent:
-		return SendAndReceiveClientEvent(sm, fm, sess, snac, r)
-	default:
-		return ErrUnimplementedSNAC
+type ICBMHandler interface {
+	ChannelMsgToHostHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)
+	ClientEventHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error
+	EvilRequestHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)
+	ParameterQueryHandler() XMessage
+}
+
+func NewICBMRouter() ICBMRouter {
+	return ICBMRouter{
+		ICBMHandler: ICBMService{},
 	}
 }
 
-func SendAndReceiveICBMParameterReply(snac oscar.SnacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("sendAndReceiveICBMParameterReply read SNAC frame: %+v\n", snac)
+type ICBMRouter struct {
+	ICBMHandler
+}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: ICBM,
-		SubGroup:  ICBMParameterReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x04_0x05_ICBMParameterReply{
-		MaxSlots:             100,
-		ICBMFlags:            3,
-		MaxIncomingICBMLen:   512,
-		MaxSourceEvil:        999,
-		MaxDestinationEvil:   999,
-		MinInterICBMInterval: 0,
+func (rt *ICBMRouter) RouteICBM(sm SessionManager, fm FeedbagManager, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch SNACFrame.SubGroup {
+	case oscar.ICBMAddParameters:
+		inSNAC := oscar.SNAC_0x04_0x02_ICBMAddParameters{}
+		return oscar.Unmarshal(&inSNAC, r)
+	case oscar.ICBMParameterQuery:
+		outSNAC := rt.ParameterQueryHandler()
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.ICBMChannelMsgToHost:
+		inSNAC := oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.ChannelMsgToHostHandler(sm, fm, sess, inSNAC)
+		if err != nil {
+			return err
+		}
+		if outSNAC != nil {
+			if err := writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w); err != nil {
+				return err
+			}
+		}
+		return nil
+	case oscar.ICBMEvilRequest:
+		inSNAC := oscar.SNAC_0x04_0x08_ICBMEvilRequest{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.EvilRequestHandler(sm, fm, sess, inSNAC)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.ICBMClientErr:
+		inSNAC := oscar.SNAC_0x04_0x0B_ICBMClientErr{}
+		return oscar.Unmarshal(&inSNAC, r)
+	case oscar.ICBMClientEvent:
+		inSNAC := oscar.SNAC_0x04_0x14_ICBMClientEvent{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		return rt.ClientEventHandler(sm, fm, sess, inSNAC)
+	default:
+		return ErrUnsupportedSubGroup
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func SendAndReceiveChannelMsgTohost(sm SessionManager, fm FeedbagManager, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("SendAndReceiveChannelMsgTohost read SNAC frame: %+v\n", snac)
+type ICBMService struct {
+}
 
-	snacPayloadIn := oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
+func (s ICBMService) ParameterQueryHandler() XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: ICBM,
+			SubGroup:  oscar.ICBMParameterReply,
+		},
+		snacOut: oscar.SNAC_0x04_0x05_ICBMParameterReply{
+			MaxSlots:             100,
+			ICBMFlags:            3,
+			MaxIncomingICBMLen:   512,
+			MaxSourceEvil:        999,
+			MaxDestinationEvil:   999,
+			MinInterICBMInterval: 0,
+		},
 	}
+}
 
+func (s ICBMService) ChannelMsgToHostHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error) {
 	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
 	if err != nil {
-		return err
+		return nil, err
 	}
+
 	if blocked != BlockedNo {
-		snacFrameOut := oscar.SnacFrame{
-			FoodGroup: ICBM,
-			SubGroup:  ICBMErr,
-		}
-		snacPayloadOut := oscar.SnacError{
-			Code: ErrorCodeNotLoggedOn,
-		}
+		code := ErrorCodeNotLoggedOn
 		if blocked == BlockedA {
-			snacPayloadOut.Code = ErrorCodeInLocalPermitDeny
+			code = ErrorCodeInLocalPermitDeny
 		}
-		return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+		return &XMessage{
+			snacFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  oscar.ICBMErr,
+			},
+			snacOut: oscar.SnacError{
+				Code: code,
+			},
+		}, nil
 	}
 
 	recipSess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
-	if err != nil {
-		if errors.Is(err, errSessNotFound) {
-			snacFrameOut := oscar.SnacFrame{
+	switch {
+	case errors.Is(err, errSessNotFound):
+		return &XMessage{
+			snacFrame: oscar.SnacFrame{
 				FoodGroup: ICBM,
-				SubGroup:  ICBMErr,
-			}
-			snacPayloadOut := oscar.SnacError{
+				SubGroup:  oscar.ICBMErr,
+			},
+			snacOut: oscar.SnacError{
 				Code: ErrorCodeNotLoggedOn,
-			}
-			return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
-		}
-		return err
+			},
+		}, nil
+	case err != nil:
+		return nil, err
 	}
 
 	clientIM := oscar.SNAC_0x04_0x07_ICBMChannelMsgToClient{
@@ -139,130 +158,89 @@ func SendAndReceiveChannelMsgTohost(sm SessionManager, fm FeedbagManager, sess *
 	sm.SendToScreenName(recipSess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: ICBM,
-			SubGroup:  ICBMChannelMsgToclient,
+			SubGroup:  oscar.ICBMChannelMsgToclient,
 		},
 		snacOut: clientIM,
 	})
 
-	if _, requestedConfirmation := snacPayloadIn.TLVRestBlock.GetSlice(ICBMTLVTagRequestHostAck); !requestedConfirmation {
+	if _, requestedConfirmation := snacPayloadIn.TLVRestBlock.GetSlice(oscar.ICBMTLVTagRequestHostAck); !requestedConfirmation {
 		// don't ack message
-		return nil
+		return nil, nil
 	}
 
 	// ack message back to sender
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: ICBM,
-		SubGroup:  ICBMHostAck,
-	}
-	snacPayloadOut := oscar.SNAC_0x04_0x0C_ICBMHostAck{
-		Cookie:     snacPayloadIn.Cookie,
-		ChannelID:  snacPayloadIn.ChannelID,
-		ScreenName: snacPayloadIn.ScreenName,
-	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
-}
-
-func ReceiveAddParameters(snac oscar.SnacFrame, r io.Reader) error {
-	fmt.Printf("ReceiveAddParameters read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x04_0x02_ICBMAddParameters{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
-	fmt.Printf("ReceiveAddParameters read SNAC: %+v\n", snacPayloadIn)
-	return nil
-}
-
-func ReceiveClientErr(snac oscar.SnacFrame, r io.Reader) error {
-	fmt.Printf("ReceiveClientErr read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x04_0x0B_ICBMClientErr{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
-	fmt.Printf("ReceiveClientErr read SNAC: %+v\n", snacPayloadIn)
-	return nil
-}
-
-func SendAndReceiveClientEvent(sm SessionManager, fm FeedbagManager, sess *Session, snac oscar.SnacFrame, r io.Reader) error {
-	fmt.Printf("SendAndReceiveClientEvent read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x04_0x14_ICBMClientEvent{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
-	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
-	if err != nil {
-		return err
-	}
-	if blocked != BlockedNo {
-		return nil
-	}
-
-	sm.SendToScreenName(snacPayloadIn.ScreenName, XMessage{
+	return &XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: ICBM,
-			SubGroup:  ICBMClientEvent,
+			SubGroup:  oscar.ICBMHostAck,
 		},
-		snacOut: oscar.SNAC_0x04_0x14_ICBMClientEvent{
+		snacOut: oscar.SNAC_0x04_0x0C_ICBMHostAck{
 			Cookie:     snacPayloadIn.Cookie,
 			ChannelID:  snacPayloadIn.ChannelID,
-			ScreenName: sess.ScreenName,
-			Event:      snacPayloadIn.Event,
+			ScreenName: snacPayloadIn.ScreenName,
 		},
-	})
-
-	return nil
+	}, nil
 }
 
-const (
-	evilDelta     = uint16(100)
-	evilDeltaAnon = uint16(30)
-)
-
-func SendAndReceiveEvilRequest(sm SessionManager, fm FeedbagManager, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("SendAndReceiveEvilRequest read SNAC frame: %+v\n", snac)
+func (s ICBMService) ClientEventHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error {
+	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
 
-	snacPayloadIn := oscar.SNAC_0x04_0x08_ICBMEvilRequest{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
+	switch {
+	case err != nil:
 		return err
+	case blocked != BlockedNo:
+		return nil
+	default:
+		sm.SendToScreenName(snacPayloadIn.ScreenName, XMessage{
+			snacFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  oscar.ICBMClientEvent,
+			},
+			snacOut: oscar.SNAC_0x04_0x14_ICBMClientEvent{
+				Cookie:     snacPayloadIn.Cookie,
+				ChannelID:  snacPayloadIn.ChannelID,
+				ScreenName: sess.ScreenName,
+				Event:      snacPayloadIn.Event,
+			},
+		})
+		return nil
 	}
+}
 
+func (s ICBMService) EvilRequestHandler(sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error) {
 	// don't let users warn themselves, it causes the AIM client to go into a
 	// weird state.
 	if snacPayloadIn.ScreenName == sess.ScreenName {
-		snacFrameOut := oscar.SnacFrame{
-			FoodGroup: ICBM,
-			SubGroup:  ICBMErr,
-		}
-		snacPayloadOut := oscar.SnacError{
-			Code: ErrorCodeNotSupportedByHost,
-		}
-		return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+		return XMessage{
+			snacFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  oscar.ICBMErr,
+			},
+			snacOut: oscar.SnacError{
+				Code: ErrorCodeNotSupportedByHost,
+			},
+		}, nil
 	}
 
 	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
 	if err != nil {
-		return err
+		return XMessage{}, nil
 	}
 	if blocked != BlockedNo {
-		snacFrameOut := oscar.SnacFrame{
-			FoodGroup: ICBM,
-			SubGroup:  ICBMErr,
-		}
-		snacPayloadOut := oscar.SnacError{
-			Code: ErrorCodeNotLoggedOn,
-		}
-		return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+		return XMessage{
+			snacFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  oscar.ICBMErr,
+			},
+			snacOut: oscar.SnacError{
+				Code: ErrorCodeNotLoggedOn,
+			},
+		}, nil
 	}
 
 	recipSess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
 	if err != nil {
-		return err
+		return XMessage{}, nil
 	}
 
 	increase := evilDelta
@@ -271,19 +249,6 @@ func SendAndReceiveEvilRequest(sm SessionManager, fm FeedbagManager, sess *Sessi
 	}
 	recipSess.IncreaseWarning(increase)
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: ICBM,
-		SubGroup:  ICBMEvilReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x04_0x09_ICBMEvilReply{
-		EvilDeltaApplied: increase,
-		UpdatedEvilValue: recipSess.GetWarning(),
-	}
-
-	if err := writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w); err != nil {
-		return err
-	}
-
 	var notif any
 	if snacPayloadIn.SendAs == 0 {
 		notif = oscar.SNAC_0x01_0x10_OServiceEvilNotification{
@@ -307,5 +272,18 @@ func SendAndReceiveEvilRequest(sm SessionManager, fm FeedbagManager, sess *Sessi
 		snacOut: notif,
 	})
 
-	return NotifyArrival(recipSess, sm, fm)
+	if err := NotifyArrival(recipSess, sm, fm); err != nil {
+		return XMessage{}, nil
+	}
+
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: ICBM,
+			SubGroup:  oscar.ICBMEvilReply,
+		},
+		snacOut: oscar.SNAC_0x04_0x09_ICBMEvilReply{
+			EvilDeltaApplied: increase,
+			UpdatedEvilValue: recipSess.GetWarning(),
+		},
+	}, nil
 }

+ 343 - 199
server/icbm_test.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 	"testing"
 )
 
@@ -16,14 +17,8 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 		// recipRetrieveErr is the error returned by the recipient session
 		// lookup
 		recipRetrieveErr error
-		// senderScreenName is the screen name of the user sending the IM
-		senderScreenName string
-		// senderWarning is the warning level of the user sending the IM
-		senderWarning uint16
-		// recipientScreenName is the screen name of the user receiving the IM
-		recipientScreenName string
-		// recipientWarning is the warning level of the user receiving the IM
-		recipientWarning uint16
+		senderSession    *Session
+		recipientSession *Session
 		// inputSNAC is the SNAC sent by the sender client
 		inputSNAC oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost
 		// expectSNACToClient is the SNAC sent from the server to the
@@ -31,24 +26,25 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 		expectSNACToClient XMessage
 		// inputSNAC is the SNAC frame sent from the server to the recipient
 		// client
-		expectSNACFrame oscar.SnacFrame
-		// expectSNACBody is the SNAC payload sent from the server to the
-		// recipient client
-		expectSNACBody any
+		expectOutput *XMessage
 	}{
 		{
-			name:                "transmit message from sender to recipient, ack message back to sender",
-			blockedState:        BlockedNo,
-			senderScreenName:    "sender-screen-name",
-			senderWarning:       10,
-			recipientScreenName: "recipient-screen-name",
-			recipientWarning:    20,
+			name:         "transmit message from sender to recipient, ack message back to sender",
+			blockedState: BlockedNo,
+			senderSession: &Session{
+				ScreenName: "sender-screen-name",
+				Warning:    10,
+			},
+			recipientSession: &Session{
+				ScreenName: "recipient-screen-name",
+				Warning:    20,
+			},
 			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				ScreenName: "recipient-screen-name",
 				TLVRestBlock: oscar.TLVRestBlock{
 					TLVList: oscar.TLVList{
 						{
-							TType: ICBMTLVTagRequestHostAck,
+							TType: oscar.ICBMTLVTagRequestHostAck,
 							Val:   []byte{},
 						},
 					},
@@ -57,7 +53,7 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 			expectSNACToClient: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: ICBM,
-					SubGroup:  ICBMChannelMsgToclient,
+					SubGroup:  oscar.ICBMChannelMsgToclient,
 				},
 				snacOut: oscar.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 					TLVUserInfo: oscar.TLVUserInfo{
@@ -67,32 +63,38 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 					TLVRestBlock: oscar.TLVRestBlock{
 						TLVList: oscar.TLVList{
 							{
-								TType: ICBMTLVTagsWantEvents,
+								TType: oscar.ICBMTLVTagsWantEvents,
 								Val:   []byte{},
 							},
 							{
-								TType: ICBMTLVTagRequestHostAck,
+								TType: oscar.ICBMTLVTagRequestHostAck,
 								Val:   []byte{},
 							},
 						},
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMHostAck,
-			},
-			expectSNACBody: oscar.SNAC_0x04_0x0C_ICBMHostAck{
-				ScreenName: "recipient-screen-name",
+			expectOutput: &XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMHostAck,
+				},
+				snacOut: oscar.SNAC_0x04_0x0C_ICBMHostAck{
+					ScreenName: "recipient-screen-name",
+				},
 			},
 		},
 		{
-			name:                "transmit message from sender to recipient, don't ack message back to sender",
-			blockedState:        BlockedNo,
-			senderScreenName:    "sender-screen-name",
-			senderWarning:       10,
-			recipientScreenName: "recipient-screen-name",
-			recipientWarning:    20,
+			name:         "transmit message from sender to recipient, don't ack message back to sender",
+			blockedState: BlockedNo,
+			senderSession: &Session{
+				ScreenName: "sender-screen-name",
+				Warning:    10,
+			},
+			recipientSession: &Session{
+				ScreenName: "recipient-screen-name",
+				Warning:    20,
+			},
 			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				ScreenName: "recipient-screen-name",
 				TLVRestBlock: oscar.TLVRestBlock{
@@ -102,7 +104,7 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 			expectSNACToClient: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: ICBM,
-					SubGroup:  ICBMChannelMsgToclient,
+					SubGroup:  oscar.ICBMChannelMsgToclient,
 				},
 				snacOut: oscar.SNAC_0x04_0x07_ICBMChannelMsgToClient{
 					TLVUserInfo: oscar.TLVUserInfo{
@@ -112,91 +114,110 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 					TLVRestBlock: oscar.TLVRestBlock{
 						TLVList: oscar.TLVList{
 							{
-								TType: ICBMTLVTagsWantEvents,
+								TType: oscar.ICBMTLVTagsWantEvents,
 								Val:   []byte{},
 							},
 						},
 					},
 				},
 			},
+			expectOutput: nil,
 		},
 		{
-			name:                "don't transmit message from sender to recipient because sender has blocked recipient",
-			blockedState:        BlockedA,
-			senderScreenName:    "sender-screen-name",
-			senderWarning:       10,
-			recipientScreenName: "recipient-screen-name",
-			recipientWarning:    20,
+			name:         "don't transmit message from sender to recipient because sender has blocked recipient",
+			blockedState: BlockedA,
+			senderSession: &Session{
+				ScreenName: "sender-screen-name",
+				Warning:    10,
+			},
+			recipientSession: &Session{
+				ScreenName: "recipient-screen-name",
+				Warning:    20,
+			},
 			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				ScreenName: "recipient-screen-name",
 				TLVRestBlock: oscar.TLVRestBlock{
 					TLVList: oscar.TLVList{
 						{
-							TType: ICBMTLVTagRequestHostAck,
+							TType: oscar.ICBMTLVTagRequestHostAck,
 							Val:   []byte{},
 						},
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMErr,
-			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeInLocalPermitDeny,
+			expectOutput: &XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeInLocalPermitDeny,
+				},
 			},
 		},
 		{
-			name:                "don't transmit message from sender to recipient because recipient has blocked sender",
-			blockedState:        BlockedB,
-			senderScreenName:    "sender-screen-name",
-			senderWarning:       10,
-			recipientScreenName: "recipient-screen-name",
-			recipientWarning:    20,
+			name:         "don't transmit message from sender to recipient because recipient has blocked sender",
+			blockedState: BlockedB,
+			senderSession: &Session{
+				ScreenName: "sender-screen-name",
+				Warning:    10,
+			},
+			recipientSession: &Session{
+				ScreenName: "recipient-screen-name",
+				Warning:    20,
+			},
 			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				ScreenName: "recipient-screen-name",
 				TLVRestBlock: oscar.TLVRestBlock{
 					TLVList: oscar.TLVList{
 						{
-							TType: ICBMTLVTagRequestHostAck,
+							TType: oscar.ICBMTLVTagRequestHostAck,
 							Val:   []byte{},
 						},
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMErr,
-			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeNotLoggedOn,
+			expectOutput: &XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotLoggedOn,
+				},
 			},
 		},
 		{
-			name:                "don't transmit message from sender to recipient because recipient doesn't exist",
-			blockedState:        BlockedNo,
-			recipRetrieveErr:    errSessNotFound,
-			senderScreenName:    "sender-screen-name",
-			senderWarning:       10,
-			recipientScreenName: "recipient-screen-name",
-			recipientWarning:    20,
+			name:             "don't transmit message from sender to recipient because recipient doesn't exist",
+			blockedState:     BlockedNo,
+			recipRetrieveErr: errSessNotFound,
+			senderSession: &Session{
+				ScreenName: "sender-screen-name",
+				Warning:    10,
+			},
+			recipientSession: &Session{
+				ScreenName: "recipient-screen-name",
+				Warning:    20,
+			},
 			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				ScreenName: "recipient-screen-name",
 				TLVRestBlock: oscar.TLVRestBlock{
 					TLVList: oscar.TLVList{
 						{
-							TType: ICBMTLVTagRequestHostAck,
+							TType: oscar.ICBMTLVTagRequestHostAck,
 							Val:   []byte{},
 						},
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMErr,
-			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeNotLoggedOn,
+			expectOutput: &XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotLoggedOn,
+				},
 			},
 		},
 	}
@@ -208,64 +229,27 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 			//
 			fm := NewMockFeedbagManager(t)
 			fm.EXPECT().
-				Blocked(tc.senderScreenName, tc.recipientScreenName).
+				Blocked(tc.senderSession.ScreenName, tc.recipientSession.ScreenName).
 				Return(tc.blockedState, nil).
 				Maybe()
 			sm := NewMockSessionManager(t)
 			sm.EXPECT().
-				RetrieveByScreenName(tc.recipientScreenName).
-				Return(&Session{
-					ScreenName: tc.recipientScreenName,
-					Warning:    tc.recipientWarning,
-				}, tc.recipRetrieveErr).
+				RetrieveByScreenName(tc.recipientSession.ScreenName).
+				Return(tc.recipientSession, tc.recipRetrieveErr).
 				Maybe()
 			sm.EXPECT().
-				SendToScreenName(tc.recipientScreenName, tc.expectSNACToClient).
+				SendToScreenName(tc.recipientSession.ScreenName, tc.expectSNACToClient).
 				Maybe()
 			//
 			// send input SNAC
 			//
-			input := &bytes.Buffer{}
-			var seq uint32
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			output := &bytes.Buffer{}
-			snac := oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMChannelMsgTohost,
-			}
-			senderSession := &Session{
-				ScreenName: tc.senderScreenName,
-				Warning:    tc.senderWarning,
-			}
-			assert.NoError(t, SendAndReceiveChannelMsgTohost(sm, fm, senderSession, snac, input, output, &seq))
+			svc := ICBMService{}
+			outputSNAC, err := svc.ChannelMsgToHostHandler(sm, fm, tc.senderSession, tc.inputSNAC)
+			assert.NoError(t, err)
 			//
 			// verify output
 			//
-			if tc.expectSNACFrame.FoodGroup == 0 {
-				// no ack was sent
-				return
-			}
-			flap := oscar.FlapFrame{}
-			assert.NoError(t, oscar.Unmarshal(&flap, output))
-			SnacFrame := oscar.SnacFrame{}
-			assert.NoError(t, oscar.Unmarshal(&SnacFrame, output))
-			assert.Equal(t, tc.expectSNACFrame, SnacFrame)
-			//
-			// verify output SNAC body
-			//
-			switch v := tc.expectSNACBody.(type) {
-			case oscar.SNAC_0x04_0x0C_ICBMHostAck:
-				outputSNAC := oscar.SNAC_0x04_0x0C_ICBMHostAck{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			case oscar.SnacError:
-				outputSNAC := oscar.SnacError{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			default:
-				t.Fatalf("unexpected output SNAC type")
-			}
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+			assert.Equal(t, tc.expectOutput, outputSNAC)
 		})
 	}
 }
@@ -297,7 +281,7 @@ func TestSendAndReceiveClientEvent(t *testing.T) {
 			expectSNACToClient: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: ICBM,
-					SubGroup:  ICBMClientEvent,
+					SubGroup:  oscar.ICBMClientEvent,
 				},
 				snacOut: oscar.SNAC_0x04_0x14_ICBMClientEvent{
 					Cookie:     [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
@@ -335,16 +319,11 @@ func TestSendAndReceiveClientEvent(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			input := &bytes.Buffer{}
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			snac := oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMChannelMsgTohost,
-			}
 			senderSession := &Session{
 				ScreenName: tc.senderScreenName,
 			}
-			assert.NoError(t, SendAndReceiveClientEvent(sm, fm, senderSession, snac, input))
+			svc := ICBMService{}
+			assert.NoError(t, svc.ClientEventHandler(sm, fm, senderSession, tc.inputSNAC))
 		})
 	}
 }
@@ -373,12 +352,8 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 		// expectSNACToClient is the SNAC sent from the server to the
 		// recipient client
 		expectSNACToClient XMessage
-		// inputSNAC is the SNAC frame sent from the server to the recipient
-		// client
-		expectSNACFrame oscar.SnacFrame
-		// expectSNACBody is the SNAC payload sent from the server to the
-		// recipient client
-		expectSNACBody any
+
+		expectOutput XMessage
 	}{
 		{
 			name:         "transmit anonymous warning from sender to recipient",
@@ -416,13 +391,15 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 					NewEvil: evilDeltaAnon,
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMEvilReply,
-			},
-			expectSNACBody: oscar.SNAC_0x04_0x09_ICBMEvilReply{
-				EvilDeltaApplied: 30,
-				UpdatedEvilValue: 30,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMEvilReply,
+				},
+				snacOut: oscar.SNAC_0x04_0x09_ICBMEvilReply{
+					EvilDeltaApplied: 30,
+					UpdatedEvilValue: 30,
+				},
 			},
 		},
 		{
@@ -465,13 +442,15 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMEvilReply,
-			},
-			expectSNACBody: oscar.SNAC_0x04_0x09_ICBMEvilReply{
-				EvilDeltaApplied: 100,
-				UpdatedEvilValue: 100,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMEvilReply,
+				},
+				snacOut: oscar.SNAC_0x04_0x09_ICBMEvilReply{
+					EvilDeltaApplied: 100,
+					UpdatedEvilValue: 100,
+				},
 			},
 		},
 		{
@@ -486,12 +465,14 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 				SendAs:     0, // make it identified
 				ScreenName: "recipient-screen-name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMErr,
-			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeNotLoggedOn,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotLoggedOn,
+				},
 			},
 		},
 		{
@@ -506,12 +487,14 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 				SendAs:     0, // make it identified
 				ScreenName: "recipient-screen-name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMErr,
-			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeNotLoggedOn,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotLoggedOn,
+				},
 			},
 		},
 		{
@@ -524,12 +507,14 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 				SendAs:     0, // make it identified
 				ScreenName: "sender-screen-name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMErr,
-			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeNotSupportedByHost,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotSupportedByHost,
+				},
 			},
 		},
 	}
@@ -566,42 +551,201 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			input := &bytes.Buffer{}
-			var seq uint32
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			output := &bytes.Buffer{}
-			snac := oscar.SnacFrame{
-				FoodGroup: ICBM,
-				SubGroup:  ICBMChannelMsgTohost,
-			}
 			senderSession := &Session{
 				ScreenName: tc.senderSession.ScreenName,
 			}
-			assert.NoError(t, SendAndReceiveEvilRequest(sm, fm, senderSession, snac, input, output, &seq))
-			//
-			// verify output
-			//
-			flap := oscar.FlapFrame{}
-			assert.NoError(t, oscar.Unmarshal(&flap, output))
-			SnacFrame := oscar.SnacFrame{}
-			assert.NoError(t, oscar.Unmarshal(&SnacFrame, output))
-			assert.Equal(t, tc.expectSNACFrame, SnacFrame)
-			//
-			// verify output SNAC body
-			//
-			switch v := tc.expectSNACBody.(type) {
-			case oscar.SNAC_0x04_0x09_ICBMEvilReply:
-				outputSNAC := oscar.SNAC_0x04_0x09_ICBMEvilReply{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			case oscar.SnacError:
-				outputSNAC := oscar.SnacError{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			default:
-				t.Fatalf("unexpected output SNAC type")
+			svc := ICBMService{}
+			outputSNAC, err := svc.EvilRequestHandler(sm, fm, senderSession, tc.inputSNAC)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.expectOutput, outputSNAC)
+		})
+	}
+}
+
+func TestICBMRouter_RouteICBM(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// input is the request payload
+		input XMessage
+		// output is the response payload
+		output XMessage
+		// handlerErr is the mocked handler error response
+		handlerErr error
+		// expectErr is the expected error returned by the router
+		expectErr error
+	}{
+		{
+			name: "receive ICBMAddParameters SNAC, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMAddParameters,
+				},
+				snacOut: oscar.SNAC_0x04_0x02_ICBMAddParameters{
+					Channel: 1,
+				},
+			},
+			output: XMessage{}, // empty SNAC
+		},
+		{
+			name: "receive ICBMParameterQuery, return ICBMParameterReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMParameterQuery,
+				},
+				snacOut: struct{}{}, // empty SNAC
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMParameterReply,
+				},
+				snacOut: oscar.SNAC_0x04_0x05_ICBMParameterReply{
+					MaxSlots: 100,
+				},
+			},
+		},
+		{
+			name: "receive ICBMChannelMsgToHost, return ICBMHostAck",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMChannelMsgToHost,
+				},
+				snacOut: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+					ScreenName: "recipient-screen-name",
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMHostAck,
+				},
+				snacOut: oscar.SNAC_0x04_0x0C_ICBMHostAck{
+					ChannelID: 4,
+				},
+			},
+		},
+		{
+			name: "receive ICBMEvilRequest, return ICBMEvilReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMEvilRequest,
+				},
+				snacOut: oscar.SNAC_0x04_0x08_ICBMEvilRequest{
+					ScreenName: "recipient-screen-name",
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMEvilReply,
+				},
+				snacOut: oscar.SNAC_0x04_0x09_ICBMEvilReply{
+					EvilDeltaApplied: 100,
+				},
+			},
+		},
+		{
+			name: "receive ICBMClientErr, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMClientErr,
+				},
+				snacOut: oscar.SNAC_0x04_0x0B_ICBMClientErr{
+					Code: 4,
+				},
+			},
+			output: XMessage{}, // empty SNAC
+		},
+		{
+			name: "receive ICBMClientEvent, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMClientEvent,
+				},
+				snacOut: oscar.SNAC_0x04_0x14_ICBMClientEvent{
+					ScreenName: "recipient-screen-name",
+				},
+			},
+			output: XMessage{}, // empty SNAC
+		},
+		{
+			name: "receive ICBMMissedCalls, expect ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMMissedCalls,
+				},
+				snacOut: struct{}{}, // empty SNAC
+			},
+			output:    XMessage{}, // empty SNAC
+			expectErr: ErrUnsupportedSubGroup,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockICBMHandler(t)
+			svc.EXPECT().
+				ChannelMsgToHostHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(&tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				ClientEventHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				EvilRequestHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				ParameterQueryHandler().
+				Return(tc.output).
+				Maybe()
+
+			router := ICBMRouter{
+				ICBMHandler: svc,
 			}
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(1)
+
+			err := router.RouteICBM(nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+
+			if tc.output.snacFrame == (oscar.SnacFrame{}) {
+				return // handler doesn't return response
+			}
+
+			// make sure the sequence number was incremented
+			assert.Equal(t, uint32(2), seq)
+
+			// verify the FLAP frame
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
+			assert.Equal(t, uint16(1), flap.Sequence)
+
+			// verify the SNAC frame
+			snacFrame := oscar.SnacFrame{}
+			assert.NoError(t, oscar.Unmarshal(&snacFrame, bufOut))
+			assert.Equal(t, tc.output.snacFrame, snacFrame)
+
+			// verify the SNAC message
+			snacBuf := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.output.snacOut, snacBuf))
+			assert.Equal(t, snacBuf.Bytes(), bufOut.Bytes())
 		})
 	}
 }

+ 1 - 1
server/locate.go

@@ -60,7 +60,7 @@ func routeLocate(sess *Session, sm SessionManager, fm *FeedbagStore, snac oscar.
 	case LocateUserInfoQuery2:
 		return SendAndReceiveUserInfoQuery2(sess, sm, fm, fm, snac, r, w, sequence)
 	default:
-		return ErrUnimplementedSNAC
+		return ErrUnsupportedSubGroup
 	}
 }
 

+ 1 - 1
server/oservice.go

@@ -66,7 +66,7 @@ func routeOService(cfg Config, ready OnReadyCB, cr *ChatRegistry, sm SessionMana
 	case OServiceSetUserinfoFields:
 		return ReceiveSetUserInfoFields(sess, sm, fm, snac, r, w, sequence)
 	default:
-		return ErrUnimplementedSNAC
+		return ErrUnsupportedSubGroup
 	}
 }
 

+ 1 - 1
server/pd.go

@@ -25,7 +25,7 @@ func routePD(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) e
 	case PDRightsQuery:
 		return SendAndReceivePDRightsQuery(snac, r, w, sequence)
 	default:
-		return ErrUnimplementedSNAC
+		return ErrUnsupportedSubGroup
 	}
 }
 

+ 17 - 5
server/protocol.go

@@ -56,7 +56,7 @@ var (
 	CapChat, _ = uuid.MustParse("748F2420-6287-11D1-8222-444553540000").MarshalBinary()
 )
 
-var ErrUnimplementedSNAC = errors.New("snac is unimplemented")
+var ErrUnsupportedSubGroup = errors.New("unimplemented subgroup, your client version may be unsupported")
 
 type Config struct {
 	BOSPort     int    `envconfig:"BOS_PORT" default:"5191"`
@@ -279,12 +279,14 @@ func ReadBos(cfg Config, ready OnReadyCB, sess *Session, seq uint32, sm SessionM
 	errCh := make(chan error, 1)
 	go readIncomingRequests(rwc, msgCh, errCh)
 
+	router := NewRouter()
+
 	for {
 		select {
 		case m := <-msgCh:
-			if err := routeIncomingRequests(cfg, ready, sm, sess, fm, cr, rwc, &seq, m.snac, m.buf); err != nil {
+			if err := router.routeIncomingRequests(cfg, ready, sm, sess, fm, cr, rwc, &seq, m.snac, m.buf); err != nil {
 				switch {
-				case errors.Is(err, ErrUnimplementedSNAC):
+				case errors.Is(err, ErrUnsupportedSubGroup):
 					if err := sendInvalidSNACErr(m.snac, rwc, &seq); err != nil {
 						return err
 					}
@@ -307,7 +309,17 @@ func ReadBos(cfg Config, ready OnReadyCB, sess *Session, seq uint32, sm SessionM
 	}
 }
 
-func routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader) error {
+func NewRouter() Router {
+	return Router{
+		ICBMRouter: NewICBMRouter(),
+	}
+}
+
+type Router struct {
+	ICBMRouter
+}
+
+func (rt *Router) routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader) error {
 	switch snac.FoodGroup {
 	case OSERVICE:
 		if err := routeOService(cfg, ready, cr, sm, fm, sess, snac, buf, rw, sequence); err != nil {
@@ -322,7 +334,7 @@ func routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionManager, sess
 			return err
 		}
 	case ICBM:
-		if err := routeICBM(sm, fm, sess, snac, buf, rw, sequence); err != nil {
+		if err := rt.RouteICBM(sm, fm, sess, snac, buf, rw, sequence); err != nil {
 			return err
 		}
 	case PD: