Mike il y a 2 ans
Parent
commit
c49ece149e

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+.idea/
+*.db

+ 26 - 16
cmd/main.go

@@ -2,11 +2,13 @@ package main
 
 import (
 	"fmt"
-	"github.com/mkaminski/goaim/handler"
-	"github.com/mkaminski/goaim/state"
+	"log/slog"
 	"os"
 	"sync"
 
+	"github.com/mkaminski/goaim/handler"
+	"github.com/mkaminski/goaim/state"
+
 	"github.com/kelseyhightower/envconfig"
 	"github.com/mkaminski/goaim/server"
 )
@@ -36,7 +38,8 @@ func main() {
 		server.StartManagementAPI(fm, logger)
 		wg.Done()
 	}()
-	go func() {
+	go func(logger *slog.Logger) {
+		logger = logger.With("svc", "BOS")
 		authHandler := handler.NewAuthService(cfg, sm, fm, fm, cr)
 		buddyHandler := handler.NewBuddyService()
 		oserviceHandler := handler.NewOServiceService(cfg, sm, fm)
@@ -47,7 +50,7 @@ func main() {
 		feedbagHandler := handler.NewFeedbagService(sm, fm)
 		icbmHandler := handler.NewICBMService(sm, fm)
 
-		bosService := server.BOSService{
+		server.BOSService{
 			AlertRouter:       server.NewAlertRouter(logger),
 			AuthHandler:       authHandler,
 			BuddyRouter:       server.NewBuddyRouter(logger, buddyHandler),
@@ -56,21 +59,21 @@ func main() {
 			ICBMRouter:        server.NewICBMRouter(logger, icbmHandler),
 			LocateRouter:      server.NewLocateRouter(locateHandler, logger),
 			OServiceBOSRouter: server.NewOServiceRouterForBOS(logger, oserviceHandler, oserviceBOSHandler),
-			Cfg:               cfg,
+			Config:            cfg,
 			RouteLogger: server.RouteLogger{
 				Logger: logger,
 			},
-		}
-		server.ListenBOS(cfg, bosService, logger.With("svc", "BOS"))
+		}.Start()
 		wg.Done()
-	}()
-	go func() {
+	}(logger)
+	go func(logger *slog.Logger) {
+		logger = logger.With("svc", "CHAT")
 		authHandler := handler.NewAuthService(cfg, sm, fm, fm, cr)
 		oserviceHandler := handler.NewOServiceService(cfg, sm, fm)
 		chatHandler := handler.NewChatService(cr)
 		oserviceChatHandler := handler.NewOServiceServiceForChat(*oserviceHandler, cr)
 
-		chatService := server.ChatService{
+		server.ChatService{
 			AuthHandler:        authHandler,
 			OServiceChatRouter: server.NewOServiceRouterForChat(logger, oserviceHandler, oserviceChatHandler),
 			ChatRouter:         server.NewChatRouter(logger, chatHandler),
@@ -78,15 +81,22 @@ func main() {
 			RouteLogger: server.RouteLogger{
 				Logger: logger,
 			},
-		}
-		server.ListenChat(cfg, chatService, logger.With("svc", "CHAT"))
+		}.Start()
 		wg.Done()
-	}()
-	go func() {
+	}(logger)
+	go func(logger *slog.Logger) {
+		logger = logger.With("svc", "AUTH")
 		authHandler := handler.NewAuthService(cfg, sm, fm, fm, cr)
-		server.ListenBUCPLogin(cfg, err, logger, authHandler)
+
+		server.AuthService{
+			AuthHandler: authHandler,
+			Config:      cfg,
+			RouteLogger: server.RouteLogger{
+				Logger: logger,
+			},
+		}.Start()
 		wg.Done()
-	}()
+	}(logger)
 
 	wg.Wait()
 }

+ 4 - 4
handler/auth.go

@@ -5,12 +5,12 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
+
 	"github.com/google/uuid"
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/server"
 	"github.com/mkaminski/goaim/state"
-	"io"
-	"net"
 )
 
 func NewAuthService(cfg server.Config, sm SessionManager, fm FeedbagManager, um UserManager, cr *state.ChatRegistry) *AuthService {
@@ -64,10 +64,10 @@ func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session, chatI
 	}
 }
 
-func (s AuthService) VerifyLogin(conn net.Conn) (*state.Session, uint32, error) {
+func (s AuthService) VerifyLogin(rwc io.ReadWriteCloser) (*state.Session, uint32, error) {
 	seq := uint32(100)
 
-	flap, err := s.SendAndReceiveSignonFrame(conn, &seq)
+	flap, err := s.SendAndReceiveSignonFrame(rwc, &seq)
 	if err != nil {
 		return nil, 0, err
 	}

+ 2 - 1
handler/auth_test.go

@@ -1,13 +1,14 @@
 package handler
 
 import (
+	"testing"
+
 	"github.com/google/uuid"
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/server"
 	"github.com/mkaminski/goaim/state"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestReceiveAndSendBUCPLoginRequest(t *testing.T) {

+ 3 - 2
handler/buddy.go

@@ -2,6 +2,7 @@ package handler
 
 import (
 	"context"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 )
@@ -79,7 +80,7 @@ func broadcastDeparture(ctx context.Context, sess *state.Session, sm SessionMana
 	return nil
 }
 
-func unicastArrival(ctx context.Context, srcScreenName, destScreenName string, sm SessionManager, ) {
+func unicastArrival(ctx context.Context, srcScreenName, destScreenName string, sm SessionManager) {
 	sess := sm.RetrieveByScreenName(srcScreenName)
 	switch {
 	case sess == nil:
@@ -98,7 +99,7 @@ func unicastArrival(ctx context.Context, srcScreenName, destScreenName string, s
 	})
 }
 
-func unicastDeparture(ctx context.Context, srcScreenName, destScreenName string, sm SessionManager, ) {
+func unicastDeparture(ctx context.Context, srcScreenName, destScreenName string, sm SessionManager) {
 	sess := sm.RetrieveByScreenName(srcScreenName)
 	switch {
 	case sess == nil:

+ 1 - 0
handler/chat.go

@@ -2,6 +2,7 @@ package handler
 
 import (
 	"context"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 )

+ 3 - 2
handler/chat_nav.go

@@ -3,11 +3,12 @@ package handler
 import (
 	"context"
 	"errors"
+	"log/slog"
+	"time"
+
 	"github.com/google/uuid"
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
-	"log/slog"
-	"time"
 )
 
 func NewChatRoom() state.ChatRoom {

+ 3 - 2
handler/chat_nav_test.go

@@ -2,11 +2,12 @@ package handler
 
 import (
 	"context"
+	"testing"
+	"time"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 	"github.com/stretchr/testify/assert"
-	"testing"
-	"time"
 )
 
 func TestSendAndReceiveCreateRoom(t *testing.T) {

+ 2 - 1
handler/chat_test.go

@@ -2,11 +2,12 @@ package handler
 
 import (
 	"context"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestSendAndReceiveChatChannelMsgToHost(t *testing.T) {

+ 2 - 1
handler/feedbag.go

@@ -2,9 +2,10 @@ package handler
 
 import (
 	"context"
+	"time"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
-	"time"
 )
 
 func NewFeedbagService(sm SessionManager, fm FeedbagManager) *FeedbagService {

+ 3 - 2
handler/feedbag_test.go

@@ -1,12 +1,13 @@
 package handler
 
 import (
+	"testing"
+	"time"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
-	"time"
 )
 
 func TestQueryHandler(t *testing.T) {

+ 1 - 0
handler/icbm.go

@@ -2,6 +2,7 @@ package handler
 
 import (
 	"context"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 )

+ 2 - 1
handler/icbm_test.go

@@ -1,11 +1,12 @@
 package handler
 
 import (
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestSendAndReceiveChannelMsgTohost(t *testing.T) {

+ 1 - 0
handler/locate.go

@@ -2,6 +2,7 @@ package handler
 
 import (
 	"context"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 )

+ 2 - 1
handler/locate_test.go

@@ -2,10 +2,11 @@ package handler
 
 import (
 	"context"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 	"github.com/stretchr/testify/assert"
-	"testing"
 )
 
 func TestSendAndReceiveUserInfoQuery2(t *testing.T) {

+ 26 - 29
handler/oservice.go

@@ -5,23 +5,24 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"time"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/server"
 	"github.com/mkaminski/goaim/state"
-	"time"
 )
 
-func NewOServiceService(cfg server.Config, sm SessionManager, fm FeedbagManager) *oServiceService {
-	return &oServiceService{cfg: cfg, sm: sm, fm: fm}
+func NewOServiceService(cfg server.Config, sm SessionManager, fm FeedbagManager) *OServiceService {
+	return &OServiceService{cfg: cfg, sessionManager: sm, feedbagManager: fm}
 }
 
-type oServiceService struct {
-	cfg server.Config
-	fm  FeedbagManager
-	sm  SessionManager
+type OServiceService struct {
+	cfg            server.Config
+	feedbagManager FeedbagManager
+	sessionManager SessionManager
 }
 
-func (s oServiceService) ClientVersionsHandler(_ context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) oscar.XMessage {
+func (s OServiceService) ClientVersionsHandler(_ context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) oscar.XMessage {
 	return oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.OSERVICE,
@@ -33,7 +34,7 @@ func (s oServiceService) ClientVersionsHandler(_ context.Context, snacPayloadIn
 	}
 }
 
-func (s oServiceService) RateParamsQueryHandler(_ context.Context) oscar.XMessage {
+func (s OServiceService) RateParamsQueryHandler(_ context.Context) oscar.XMessage {
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: oscar.OSERVICE,
 		SubGroup:  oscar.OServiceRateParamsReply,
@@ -100,7 +101,7 @@ func (s oServiceService) RateParamsQueryHandler(_ context.Context) oscar.XMessag
 	}
 }
 
-func (s oServiceService) UserInfoQueryHandler(_ context.Context, sess *state.Session) oscar.XMessage {
+func (s OServiceService) UserInfoQueryHandler(_ context.Context, sess *state.Session) oscar.XMessage {
 	return oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.OSERVICE,
@@ -112,17 +113,17 @@ func (s oServiceService) UserInfoQueryHandler(_ context.Context, sess *state.Ses
 	}
 }
 
-func (s oServiceService) SetUserInfoFieldsHandler(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (oscar.XMessage, error) {
+func (s OServiceService) SetUserInfoFieldsHandler(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (oscar.XMessage, error) {
 	if status, hasStatus := snacPayloadIn.GetUint32(0x06); hasStatus {
 		switch status {
 		case 0x000:
 			sess.SetInvisible(false)
-			if err := broadcastArrival(ctx, sess, s.sm, s.fm); err != nil {
+			if err := broadcastArrival(ctx, sess, s.sessionManager, s.feedbagManager); err != nil {
 				return oscar.XMessage{}, err
 			}
 		case 0x100:
 			sess.SetInvisible(true)
-			if err := broadcastDeparture(ctx, sess, s.sm, s.fm); err != nil {
+			if err := broadcastDeparture(ctx, sess, s.sessionManager, s.feedbagManager); err != nil {
 				return oscar.XMessage{}, err
 			}
 		default:
@@ -140,29 +141,29 @@ func (s oServiceService) SetUserInfoFieldsHandler(ctx context.Context, sess *sta
 	}, nil
 }
 
-func (s oServiceService) IdleNotificationHandler(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
+func (s OServiceService) IdleNotificationHandler(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
 	if snacPayloadIn.IdleTime == 0 {
 		sess.SetActive()
 	} else {
 		sess.SetIdle(time.Duration(snacPayloadIn.IdleTime) * time.Second)
 	}
-	return broadcastArrival(ctx, sess, s.sm, s.fm)
+	return broadcastArrival(ctx, sess, s.sessionManager, s.feedbagManager)
 }
 
 // RateParamsSubAddHandler exists to capture the SNAC input in unit tests to
 // verify it's correctly unmarshalled.
-func (s oServiceService) RateParamsSubAddHandler(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
+func (s OServiceService) RateParamsSubAddHandler(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
 }
 
-func NewOServiceServiceForBOS(oserviceService oServiceService, cr *state.ChatRegistry) *OServiceServiceForBOS {
+func NewOServiceServiceForBOS(oserviceService OServiceService, cr *state.ChatRegistry) *OServiceServiceForBOS {
 	return &OServiceServiceForBOS{
-		oServiceService: oserviceService,
+		OServiceService: oserviceService,
 		cr:              cr,
 	}
 }
 
 type OServiceServiceForBOS struct {
-	oServiceService
+	OServiceService
 	cr *state.ChatRegistry
 }
 
@@ -230,35 +231,31 @@ func (s OServiceServiceForBOS) WriteOServiceHostOnline() oscar.XMessage {
 }
 
 func (s OServiceServiceForBOS) ClientOnlineHandler(ctx context.Context, _ oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *state.Session) error {
-	if err := broadcastArrival(ctx, sess, s.sm, s.fm); err != nil {
+	if err := broadcastArrival(ctx, sess, s.sessionManager, s.feedbagManager); err != nil {
 		return err
 	}
-	buddies, err := s.fm.Buddies(sess.ScreenName())
+	buddies, err := s.feedbagManager.Buddies(sess.ScreenName())
 	if err != nil {
 		return err
 	}
 	for _, buddy := range buddies {
-		unicastArrival(ctx, buddy, sess.ScreenName(), s.sm)
+		unicastArrival(ctx, buddy, sess.ScreenName(), s.sessionManager)
 	}
 	return nil
 }
 
-func NewOServiceServiceForChat(oserviceService oServiceService, chatRegistry *state.ChatRegistry) *OServiceServiceForChat {
+func NewOServiceServiceForChat(oserviceService OServiceService, chatRegistry *state.ChatRegistry) *OServiceServiceForChat {
 	return &OServiceServiceForChat{
-		oServiceService: oserviceService,
+		OServiceService: oserviceService,
 		chatRegistry:    chatRegistry,
 	}
 }
 
 type OServiceServiceForChat struct {
-	oServiceService
+	OServiceService
 	chatRegistry *state.ChatRegistry
 }
 
-func (s OServiceServiceForChat) ServiceRequestHandler(_ context.Context, _ *state.Session, _ oscar.SNAC_0x01_0x04_OServiceServiceRequest) (oscar.XMessage, error) {
-	return oscar.XMessage{}, server.ErrUnsupportedSubGroup
-}
-
 func (s OServiceServiceForChat) WriteOServiceHostOnline() oscar.XMessage {
 	return oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{

+ 6 - 5
handler/oservice_test.go

@@ -1,12 +1,13 @@
 package handler
 
 import (
+	"testing"
+	"time"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/server"
 	"github.com/mkaminski/goaim/state"
 	"github.com/stretchr/testify/assert"
-	"testing"
-	"time"
 )
 
 func TestReceiveAndSendServiceRequest(t *testing.T) {
@@ -126,9 +127,9 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 			// send input SNAC
 			//
 			svc := OServiceServiceForBOS{
-				oServiceService: oServiceService{
-					cfg: tc.cfg,
-					sm:  sm,
+				OServiceService: OServiceService{
+					cfg:            tc.cfg,
+					sessionManager: sm,
 				},
 				cr: cr,
 			}

+ 2 - 1
handler/test_helpers.go

@@ -1,8 +1,9 @@
 package handler
 
 import (
-	"github.com/mkaminski/goaim/state"
 	"time"
+
+	"github.com/mkaminski/goaim/state"
 )
 
 // sessOptWarning sets a warning level on the session object

+ 2 - 1
handler/types.go

@@ -2,9 +2,10 @@ package handler
 
 import (
 	"context"
+	"time"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
-	"time"
 )
 
 type FeedbagManager interface {

+ 16 - 8
oscar/fixtures/blocked_user.sql

@@ -1,8 +1,16 @@
-INSERT INTO "feedbag" VALUES('userA',0,0,1,'',X'00C800026BEF',1690508233);
-INSERT INTO "feedbag" VALUES('userA',0,13852,3,'userB',NULL,1691286176);
-INSERT INTO "feedbag" VALUES('userA',0,31420,4,'',X'00CA000104',1691286176);
-INSERT INTO "feedbag" VALUES('userA',27631,0,1,'Friends!',X'00C800020FB0',1691286169);
-INSERT INTO "feedbag" VALUES('userA',27631,4016,0,'userB',NULL,1690508233);
-INSERT INTO "feedbag" VALUES('userB',0,0,1,'',X'00C800026EAA',1690508298);
-INSERT INTO "feedbag" VALUES('userB',28330,0,1,'Enemies',X'00C800021FB8',1691180328);
-INSERT INTO "feedbag" VALUES('userB',28330,8120,0,'userA',NULL,1691180328);
+INSERT INTO "feedbag"
+VALUES ('userA', 0, 0, 1, '', X'00C800026BEF', 1690508233);
+INSERT INTO "feedbag"
+VALUES ('userA', 0, 13852, 3, 'userB', NULL, 1691286176);
+INSERT INTO "feedbag"
+VALUES ('userA', 0, 31420, 4, '', X'00CA000104', 1691286176);
+INSERT INTO "feedbag"
+VALUES ('userA', 27631, 0, 1, 'Friends!', X'00C800020FB0', 1691286169);
+INSERT INTO "feedbag"
+VALUES ('userA', 27631, 4016, 0, 'userB', NULL, 1690508233);
+INSERT INTO "feedbag"
+VALUES ('userB', 0, 0, 1, '', X'00C800026EAA', 1690508298);
+INSERT INTO "feedbag"
+VALUES ('userB', 28330, 0, 1, 'Enemies', X'00C800021FB8', 1691180328);
+INSERT INTO "feedbag"
+VALUES ('userB', 28330, 8120, 0, 'userA', NULL, 1691180328);

+ 2 - 1
server/alert.go

@@ -2,8 +2,9 @@ package server
 
 import (
 	"context"
-	"github.com/mkaminski/goaim/oscar"
 	"log/slog"
+
+	"github.com/mkaminski/goaim/oscar"
 )
 
 func NewAlertRouter(logger *slog.Logger) AlertRouter {

+ 2 - 1
server/alert_test.go

@@ -3,9 +3,10 @@ package server
 import (
 	"bytes"
 	"context"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
-	"testing"
 )
 
 func TestAlertRouter_RouteAlert(t *testing.T) {

+ 128 - 0
server/auth_service.go

@@ -0,0 +1,128 @@
+package server
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"net"
+	"os"
+
+	"github.com/google/uuid"
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
+)
+
+type AuthHandler interface {
+	ReceiveAndSendAuthChallenge(snacPayloadIn oscar.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (oscar.XMessage, error)
+	ReceiveAndSendBUCPLoginRequest(snacPayloadIn oscar.SNAC_0x17_0x02_BUCPLoginRequest, newUUID func() uuid.UUID) (oscar.XMessage, error)
+	RetrieveChatSession(ctx context.Context, chatID string, sessID string) (*state.Session, error)
+	SendAndReceiveSignonFrame(rw io.ReadWriter, sequence *uint32) (oscar.FlapSignonFrame, error)
+	Signout(ctx context.Context, sess *state.Session) error
+	SignoutChat(ctx context.Context, sess *state.Session, chatID string)
+	VerifyChatLogin(rw io.ReadWriter) (*ChatCookie, uint32, error)
+	VerifyLogin(rwc io.ReadWriteCloser) (*state.Session, uint32, error)
+}
+
+type AuthService struct {
+	AuthHandler
+	Config
+	RouteLogger
+}
+
+func (rt AuthService) handleAuthConnection(rwc io.ReadWriteCloser) {
+	defer rwc.Close()
+	seq := uint32(100)
+	_, err := rt.SendAndReceiveSignonFrame(rwc, &seq)
+	if err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+
+	flap := oscar.FlapFrame{}
+	if err := oscar.Unmarshal(&flap, rwc); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+	b := make([]byte, flap.PayloadLength)
+	if _, err := rwc.Read(b); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+	snac := oscar.SnacFrame{}
+	buf := bytes.NewBuffer(b)
+	if err := oscar.Unmarshal(&snac, buf); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+
+	snacPayloadIn := oscar.SNAC_0x17_0x06_BUCPChallengeRequest{}
+	if err := oscar.Unmarshal(&snacPayloadIn, buf); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+
+	msg, err := rt.ReceiveAndSendAuthChallenge(snacPayloadIn, uuid.New)
+	if err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+	if err := sendSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, rwc); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+
+	flap = oscar.FlapFrame{}
+	if err := oscar.Unmarshal(&flap, rwc); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+	snac = oscar.SnacFrame{}
+	b = make([]byte, flap.PayloadLength)
+	if _, err := rwc.Read(b); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+	buf = bytes.NewBuffer(b)
+	if err := oscar.Unmarshal(&snac, buf); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+
+	snacPayloadIn2 := oscar.SNAC_0x17_0x02_BUCPLoginRequest{}
+	if err := oscar.Unmarshal(&snacPayloadIn2, buf); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+
+	msg, err = rt.ReceiveAndSendBUCPLoginRequest(snacPayloadIn2, uuid.New)
+	if err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+	if err := sendSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, rwc); err != nil {
+		rt.Logger.Error(err.Error())
+		return
+	}
+}
+
+func (rt AuthService) Start() {
+	addr := Address("", rt.Config.OSCARPort)
+	listener, err := net.Listen("tcp", addr)
+	if err != nil {
+		rt.Logger.Error("unable to bind OSCAR server address", "err", err.Error())
+		os.Exit(1)
+	}
+	defer listener.Close()
+
+	rt.Logger.Info("starting auth service", "addr", addr)
+
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			rt.Logger.Error(err.Error())
+			continue
+		}
+
+		go rt.handleAuthConnection(conn)
+	}
+}

+ 128 - 0
server/bos_service.go

@@ -0,0 +1,128 @@
+package server
+
+import (
+	"context"
+	"errors"
+	"io"
+	"net"
+	"os"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
+)
+
+type BOSService struct {
+	AlertRouter
+	AuthHandler
+	BuddyRouter
+	ChatNavRouter
+	Config
+	FeedbagRouter
+	ICBMRouter
+	LocateRouter
+	OServiceBOSRouter
+	RouteLogger
+}
+
+func (rt BOSService) Start() {
+	addr := Address("", rt.Config.BOSPort)
+	listener, err := net.Listen("tcp", addr)
+	if err != nil {
+		rt.Logger.Error("unable to bind BOS server address", "err", err.Error())
+		os.Exit(1)
+	}
+	defer listener.Close()
+
+	rt.Logger.Info("starting BOS service", "addr", addr)
+
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			rt.Logger.Error(err.Error())
+			continue
+		}
+		ctx := context.Background()
+		ctx = context.WithValue(ctx, "ip", conn.RemoteAddr().String())
+		rt.Logger.DebugContext(ctx, "accepted connection")
+		go rt.handleNewConnection(ctx, conn)
+	}
+}
+
+func (rt BOSService) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser) {
+	sess, seq, err := rt.VerifyLogin(rwc)
+	if err != nil {
+		rt.Logger.ErrorContext(ctx, "user disconnected with error", "err", err.Error())
+		return
+	}
+
+	defer sess.Close()
+	defer rwc.Close()
+
+	go func() {
+		<-sess.Closed()
+		if err := rt.Signout(ctx, sess); err != nil {
+			rt.Logger.ErrorContext(ctx, "error notifying departure", "err", err.Error())
+		}
+	}()
+
+	ctx = context.WithValue(ctx, "screenName", sess.ScreenName())
+
+	msg := rt.WriteOServiceHostOnline()
+	if err := sendSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, rwc); err != nil {
+		rt.Logger.ErrorContext(ctx, "error WriteOServiceHostOnline")
+		return
+	}
+
+	fnClientReqHandler := func(ctx context.Context, r io.Reader, w io.Writer, seq *uint32) error {
+		return rt.route(ctx, sess, r, w, seq)
+	}
+	fnAlertHandler := func(ctx context.Context, msg oscar.XMessage, w io.Writer, seq *uint32) error {
+		return sendSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, seq, w)
+	}
+	dispatchIncomingMessages(ctx, sess, seq, rwc, rt.Logger, fnClientReqHandler, fnAlertHandler)
+}
+
+func (rt BOSService) route(ctx context.Context, sess *state.Session, r io.Reader, w io.Writer, sequence *uint32) error {
+	snac := oscar.SnacFrame{}
+	if err := oscar.Unmarshal(&snac, r); err != nil {
+		return err
+	}
+
+	err := func() error {
+		switch snac.FoodGroup {
+		case oscar.OSERVICE:
+			return rt.RouteOService(ctx, sess, snac, r, w, sequence)
+		case oscar.LOCATE:
+			return rt.RouteLocate(ctx, sess, snac, r, w, sequence)
+		case oscar.BUDDY:
+			return rt.RouteBuddy(ctx, snac, r, w, sequence)
+		case oscar.ICBM:
+			return rt.RouteICBM(ctx, sess, snac, r, w, sequence)
+		case oscar.CHAT_NAV:
+			return rt.RouteChatNav(ctx, sess, snac, r, w, sequence)
+		case oscar.FEEDBAG:
+			return rt.RouteFeedbag(ctx, sess, snac, r, w, sequence)
+		case oscar.BUCP:
+			return routeBUCP(ctx)
+		case oscar.ALERT:
+			return rt.RouteAlert(ctx, snac)
+		default:
+			return ErrUnsupportedSubGroup
+		}
+	}()
+
+	if err != nil {
+		rt.logRequestError(ctx, snac, err)
+		if errors.Is(err, ErrUnsupportedSubGroup) {
+			if err1 := sendInvalidSNACErr(snac, w, sequence); err1 != nil {
+				err = errors.Join(err1, err)
+			}
+			if rt.Config.FailFast {
+				panic(err.Error())
+			}
+			return nil
+		}
+	}
+
+	return err
+}

+ 1 - 1
server/buddy.go

@@ -35,7 +35,7 @@ func (rt *BuddyRouter) RouteBuddy(ctx context.Context, SNACFrame oscar.SnacFrame
 		}
 		outSNAC := rt.RightsQueryHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}

+ 2 - 1
server/buddy_test.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"bytes"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestBuddyRouter_RouteBuddy(t *testing.T) {

+ 4 - 3
server/chat.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"context"
-	"github.com/mkaminski/goaim/oscar"
-	"github.com/mkaminski/goaim/state"
 	"io"
 	"log/slog"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
 )
 
 type ChatHandler interface {
@@ -42,7 +43,7 @@ func (rt *ChatRouter) RouteChat(ctx context.Context, sess *state.Session, chatID
 		}
 		rt.Logger.InfoContext(ctx, "user sent a chat message")
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}

+ 6 - 5
server/chat_nav.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"context"
-	"github.com/mkaminski/goaim/oscar"
-	"github.com/mkaminski/goaim/state"
 	"io"
 	"log/slog"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
 )
 
 type ChatNavHandler interface {
@@ -33,7 +34,7 @@ func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *state.Session,
 	case oscar.ChatNavRequestChatRights:
 		outSNAC := rt.RequestChatRightsHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, nil, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.ChatNavRequestRoomInfo:
 		inSNAC := oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -44,7 +45,7 @@ func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *state.Session,
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.ChatNavCreateRoom:
 		inSNAC := oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -57,7 +58,7 @@ func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *state.Session,
 		roomName, _ := inSNAC.GetString(oscar.ChatTLVRoomName)
 		rt.Logger.InfoContext(ctx, "user started a chat room", slog.String("roomName", roomName))
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}

+ 2 - 1
server/chat_nav_test.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"bytes"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestChatNavRouter_RouteChatNavRouter(t *testing.T) {

+ 118 - 0
server/chat_service.go

@@ -0,0 +1,118 @@
+package server
+
+import (
+	"context"
+	"errors"
+	"io"
+	"net"
+	"os"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
+)
+
+type ChatService struct {
+	AuthHandler
+	ChatRouter
+	Config
+	OServiceChatRouter
+	RouteLogger
+}
+
+func (rt ChatService) Start() {
+	addr := Address("", rt.Config.ChatPort)
+	listener, err := net.Listen("tcp", addr)
+	if err != nil {
+		rt.Logger.Error("unable to bind chat server address", "err", err.Error())
+		os.Exit(1)
+	}
+	defer listener.Close()
+
+	rt.Logger.Info("starting chat service", "addr", addr)
+
+	for {
+		conn, err := listener.Accept()
+		if err != nil {
+			rt.Logger.Error(err.Error())
+			continue
+		}
+		ctx := context.Background()
+		ctx = context.WithValue(ctx, "ip", conn.RemoteAddr().String())
+		rt.Logger.DebugContext(ctx, "accepted connection")
+		go func() {
+			rt.handleNewConnection(ctx, conn)
+			conn.Close()
+		}()
+	}
+}
+
+func (rt ChatService) handleNewConnection(ctx context.Context, rw io.ReadWriter) {
+	cookie, seq, err := rt.VerifyChatLogin(rw)
+	if err != nil {
+		rt.Logger.ErrorContext(ctx, "user disconnected with error", "err", err.Error())
+		return
+	}
+
+	chatID := string(cookie.Cookie)
+
+	chatSess, err := rt.RetrieveChatSession(ctx, chatID, cookie.SessID)
+	if err != nil {
+		rt.Logger.ErrorContext(ctx, "unable to find chat room", "err", err.Error())
+		return
+	}
+
+	defer chatSess.Close()
+	go func() {
+		<-chatSess.Closed()
+		rt.SignoutChat(ctx, chatSess, chatID)
+	}()
+
+	ctx = context.WithValue(ctx, "screenName", chatSess.ScreenName())
+
+	msg := rt.WriteOServiceHostOnline()
+	if err := sendSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, rw); err != nil {
+		rt.Logger.ErrorContext(ctx, "error WriteOServiceHostOnline")
+		return
+	}
+
+	fnClientReqHandler := func(ctx context.Context, r io.Reader, w io.Writer, seq *uint32) error {
+		return rt.route(ctx, chatSess, r, w, seq, chatID)
+	}
+	fnAlertHandler := func(ctx context.Context, msg oscar.XMessage, w io.Writer, seq *uint32) error {
+		return sendSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, seq, w)
+	}
+	dispatchIncomingMessages(ctx, chatSess, seq, rw, rt.Logger, fnClientReqHandler, fnAlertHandler)
+}
+
+func (rt ChatService) route(ctx context.Context, sess *state.Session, r io.Reader, w io.Writer, sequence *uint32, chatID string) error {
+	snac := oscar.SnacFrame{}
+	if err := oscar.Unmarshal(&snac, r); err != nil {
+		return err
+	}
+
+	err := func() error {
+		switch snac.FoodGroup {
+		case oscar.OSERVICE:
+			return rt.RouteOService(ctx, sess, chatID, snac, r, w, sequence)
+		case oscar.CHAT:
+			return rt.RouteChat(ctx, sess, chatID, snac, r, w, sequence)
+		default:
+			return ErrUnsupportedSubGroup
+		}
+	}()
+
+	if err != nil {
+		rt.logRequestError(ctx, snac, err)
+		if errors.Is(err, ErrUnsupportedSubGroup) {
+			if err1 := sendInvalidSNACErr(snac, w, sequence); err1 != nil {
+				err = errors.Join(err1, err)
+			}
+			if rt.Config.FailFast {
+				panic(err.Error())
+			}
+			return nil
+		}
+	}
+
+	return err
+}

+ 1 - 1
server/chat_test.go

@@ -2,11 +2,11 @@ package server
 
 import (
 	"bytes"
-	"github.com/stretchr/testify/mock"
 	"testing"
 
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func TestChatRouter_RouteChat(t *testing.T) {

+ 48 - 226
server/connection.go

@@ -5,12 +5,8 @@ import (
 	"context"
 	"errors"
 	"io"
-	"log"
 	"log/slog"
-	"net"
-	"os"
 
-	"github.com/google/uuid"
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/mkaminski/goaim/state"
 )
@@ -28,6 +24,54 @@ type (
 	clientReqHandler func(ctx context.Context, r io.Reader, w io.Writer, u *uint32) error
 )
 
+func sendSNAC(originsnac oscar.SnacFrame, snacFrame oscar.SnacFrame, snacOut any, sequence *uint32, w io.Writer) error {
+	if originsnac.RequestID != 0 {
+		snacFrame.RequestID = originsnac.RequestID
+	}
+
+	snacBuf := &bytes.Buffer{}
+	if err := oscar.Marshal(snacFrame, snacBuf); err != nil {
+		return err
+	}
+	if err := oscar.Marshal(snacOut, snacBuf); err != nil {
+		return err
+	}
+
+	flap := oscar.FlapFrame{
+		StartMarker:   42,
+		FrameType:     oscar.FlapFrameData,
+		Sequence:      uint16(*sequence),
+		PayloadLength: uint16(snacBuf.Len()),
+	}
+
+	if err := oscar.Marshal(flap, w); err != nil {
+		return err
+	}
+
+	expectLen := snacBuf.Len()
+	c, err := w.Write(snacBuf.Bytes())
+	if err != nil {
+		return err
+	}
+	if c != expectLen {
+		panic("did not write the expected # of bytes")
+	}
+
+	*sequence++
+	return nil
+}
+
+func sendInvalidSNACErr(snac oscar.SnacFrame, w io.Writer, sequence *uint32) error {
+	snacFrameOut := oscar.SnacFrame{
+		FoodGroup: snac.FoodGroup,
+		SubGroup:  0x01, // error subgroup for all SNACs
+	}
+	snacPayloadOut := oscar.SnacError{
+		Code: oscar.ErrorCodeInvalidSnac,
+	}
+	return sendSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+}
+
 func consumeFLAPFrames(r io.Reader, msgCh chan incomingMessage, errCh chan error) {
 	defer close(msgCh)
 	defer close(errCh)
@@ -114,225 +158,3 @@ func dispatchIncomingMessages(ctx context.Context, sess *state.Session, seq uint
 		}
 	}
 }
-
-func handleChatConnection(ctx context.Context, rw io.ReadWriter, serviceManager ChatServiceManager, logger *slog.Logger) {
-	cookie, seq, err := serviceManager.VerifyChatLogin(rw)
-	if err != nil {
-		logger.ErrorContext(ctx, "user disconnected with error", "err", err.Error())
-		return
-	}
-
-	chatID := string(cookie.Cookie)
-
-	chatSess, err := serviceManager.RetrieveChatSession(ctx, chatID, cookie.SessID)
-	if err != nil {
-		logger.ErrorContext(ctx, "unable to find chat room", "err", err.Error())
-		return
-	}
-
-	defer chatSess.Close()
-	go func() {
-		<-chatSess.Closed()
-		serviceManager.SignoutChat(ctx, chatSess, chatID)
-	}()
-
-	ctx = context.WithValue(ctx, "screenName", chatSess.ScreenName())
-
-	msg := serviceManager.WriteOServiceHostOnline()
-	if err := writeOutSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, rw); err != nil {
-		logger.ErrorContext(ctx, "error WriteOServiceHostOnline")
-		return
-	}
-
-	fnClientReqHandler := func(ctx context.Context, r io.Reader, w io.Writer, seq *uint32) error {
-		return serviceManager.Route(ctx, chatSess, r, w, seq, chatID)
-	}
-	fnAlertHandler := func(ctx context.Context, msg oscar.XMessage, w io.Writer, seq *uint32) error {
-		return writeOutSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, seq, w)
-	}
-	dispatchIncomingMessages(ctx, chatSess, seq, rw, logger, fnClientReqHandler, fnAlertHandler)
-}
-
-func handleAuthConnection(authHandler AuthHandler, conn net.Conn) {
-	defer conn.Close()
-	seq := uint32(100)
-	_, err := authHandler.SendAndReceiveSignonFrame(conn, &seq)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-
-	flap := oscar.FlapFrame{}
-	if err := oscar.Unmarshal(&flap, conn); err != nil {
-		log.Println(err)
-		return
-	}
-	b := make([]byte, flap.PayloadLength)
-	if _, err := conn.Read(b); err != nil {
-		log.Println(err)
-		return
-	}
-	snac := oscar.SnacFrame{}
-	buf := bytes.NewBuffer(b)
-	if err := oscar.Unmarshal(&snac, buf); err != nil {
-		log.Println(err)
-		return
-	}
-
-	snacPayloadIn := oscar.SNAC_0x17_0x06_BUCPChallengeRequest{}
-	if err := oscar.Unmarshal(&snacPayloadIn, buf); err != nil {
-		log.Println(err)
-		return
-	}
-
-	msg, err := authHandler.ReceiveAndSendAuthChallenge(snacPayloadIn, uuid.New)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-	if err := writeOutSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, conn); err != nil {
-		log.Println(err)
-		return
-	}
-
-	flap = oscar.FlapFrame{}
-	if err := oscar.Unmarshal(&flap, conn); err != nil {
-		log.Println(err)
-		return
-	}
-	snac = oscar.SnacFrame{}
-	b = make([]byte, flap.PayloadLength)
-	if _, err := conn.Read(b); err != nil {
-		log.Println(err)
-		return
-	}
-	buf = bytes.NewBuffer(b)
-	if err := oscar.Unmarshal(&snac, buf); err != nil {
-		log.Println(err)
-		return
-	}
-
-	snacPayloadIn2 := oscar.SNAC_0x17_0x02_BUCPLoginRequest{}
-	if err := oscar.Unmarshal(&snacPayloadIn2, buf); err != nil {
-		log.Println(err)
-		return
-	}
-
-	msg, err = authHandler.ReceiveAndSendBUCPLoginRequest(snacPayloadIn2, uuid.New)
-	if err != nil {
-		log.Println(err)
-		return
-	}
-	if err := writeOutSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, conn); err != nil {
-		log.Println(err)
-		return
-	}
-}
-
-func handleBOSConnection(ctx context.Context, conn net.Conn, serviceManager BOSServiceManager, logger *slog.Logger) {
-	// todo why is conn net.Conn but handleChat is rw?
-	sess, seq, err := serviceManager.VerifyLogin(conn)
-	if err != nil {
-		logger.ErrorContext(ctx, "user disconnected with error", "err", err.Error())
-		return
-	}
-
-	defer sess.Close()
-	defer conn.Close()
-
-	go func() {
-		<-sess.Closed()
-		if err := serviceManager.Signout(ctx, sess); err != nil {
-			logger.ErrorContext(ctx, "error notifying departure", "err", err.Error())
-		}
-	}()
-
-	ctx = context.WithValue(ctx, "screenName", sess.ScreenName())
-
-	msg := serviceManager.WriteOServiceHostOnline()
-	if err := writeOutSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, &seq, conn); err != nil {
-		logger.ErrorContext(ctx, "error WriteOServiceHostOnline")
-		return
-	}
-
-	fnClientReqHandler := func(ctx context.Context, r io.Reader, w io.Writer, seq *uint32) error {
-		return serviceManager.Route(ctx, sess, r, w, seq)
-	}
-	fnAlertHandler := func(ctx context.Context, msg oscar.XMessage, w io.Writer, seq *uint32) error {
-		return writeOutSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, seq, w)
-	}
-	dispatchIncomingMessages(ctx, sess, seq, conn, logger, fnClientReqHandler, fnAlertHandler)
-}
-
-func ListenChat(cfg Config, router ChatServiceManager, logger *slog.Logger) {
-	addr := Address("", cfg.ChatPort)
-	listener, err := net.Listen("tcp", addr)
-	if err != nil {
-		logger.Error("unable to bind chat server address", "err", err.Error())
-		os.Exit(1)
-	}
-	defer listener.Close()
-
-	logger.Info("starting service", "addr", addr)
-
-	for {
-		conn, err := listener.Accept()
-		if err != nil {
-			log.Println(err)
-			continue
-		}
-		ctx := context.Background()
-		ctx = context.WithValue(ctx, "ip", conn.RemoteAddr().String())
-		logger.DebugContext(ctx, "accepted connection")
-		go func() {
-			handleChatConnection(ctx, conn, router, logger)
-			conn.Close()
-		}()
-	}
-}
-
-func ListenBOS(cfg Config, router BOSServiceManager, logger *slog.Logger) {
-	addr := Address("", cfg.BOSPort)
-	listener, err := net.Listen("tcp", addr)
-	if err != nil {
-		logger.Error("unable to bind BOS server address", "err", err.Error())
-		os.Exit(1)
-	}
-	defer listener.Close()
-
-	logger.Info("starting service", "addr", addr)
-
-	for {
-		conn, err := listener.Accept()
-		if err != nil {
-			log.Println(err)
-			continue
-		}
-		ctx := context.Background()
-		ctx = context.WithValue(ctx, "ip", conn.RemoteAddr().String())
-		logger.DebugContext(ctx, "accepted connection")
-		go handleBOSConnection(ctx, conn, router, logger)
-	}
-}
-
-func ListenBUCPLogin(cfg Config, err error, logger *slog.Logger, authHandler AuthHandler) {
-	addr := Address("", cfg.OSCARPort)
-	listener, err := net.Listen("tcp", addr)
-	if err != nil {
-		logger.Error("unable to bind OSCAR server address", "err", err.Error())
-		os.Exit(1)
-	}
-	defer listener.Close()
-
-	logger.Info("starting OSCAR server", "addr", addr)
-
-	for {
-		conn, err := listener.Accept()
-		if err != nil {
-			log.Println(err)
-			continue
-		}
-
-		go handleAuthConnection(authHandler, conn)
-	}
-}

+ 4 - 3
server/connection_test.go

@@ -4,12 +4,13 @@ import (
 	"bufio"
 	"bytes"
 	"context"
-	"github.com/mkaminski/goaim/oscar"
-	"github.com/mkaminski/goaim/state"
-	"github.com/stretchr/testify/assert"
 	"io"
 	"sync"
 	"testing"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestHandleChatConnection_Notification(t *testing.T) {

+ 9 - 8
server/feedbag.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"context"
-	"github.com/mkaminski/goaim/oscar"
-	"github.com/mkaminski/goaim/state"
 	"io"
 	"log/slog"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
 )
 
 type FeedbagHandler interface {
@@ -41,14 +42,14 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sess *state.Session, S
 		}
 		outSNAC := rt.RightsQueryHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.FeedbagQuery:
 		inSNAC, err := rt.QueryHandler(ctx, sess)
 		if err != nil {
 			return err
 		}
 		rt.logRequest(ctx, SNACFrame, inSNAC)
-		return writeOutSNAC(SNACFrame, inSNAC.SnacFrame, inSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, inSNAC.SnacFrame, inSNAC.SnacOut, sequence, w)
 	case oscar.FeedbagQueryIfModified:
 		inSNAC := oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -59,7 +60,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sess *state.Session, S
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.FeedbagUse:
 		rt.logRequest(ctx, SNACFrame, nil)
 		return nil
@@ -73,7 +74,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sess *state.Session, S
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.FeedbagUpdateItem:
 		inSNAC := oscar.SNAC_0x13_0x09_FeedbagUpdateItem{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -84,7 +85,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sess *state.Session, S
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.FeedbagDeleteItem:
 		inSNAC := oscar.SNAC_0x13_0x0A_FeedbagDeleteItem{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -95,7 +96,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sess *state.Session, S
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.FeedbagStartCluster:
 		inSNAC := oscar.SNAC_0x13_0x11_FeedbagStartCluster{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {

+ 2 - 1
server/feedbag_test.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"bytes"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestFeedbagRouter_RouteFeedbag(t *testing.T) {

+ 4 - 4
server/icbm.go

@@ -2,11 +2,11 @@ package server
 
 import (
 	"context"
-	"github.com/mkaminski/goaim/state"
 	"io"
 	"log/slog"
 
 	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
 )
 
 type ICBMHandler interface {
@@ -39,7 +39,7 @@ func (rt *ICBMRouter) RouteICBM(ctx context.Context, sess *state.Session, SNACFr
 	case oscar.ICBMParameterQuery:
 		outSNAC := rt.ParameterQueryHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, outSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.ICBMChannelMsgToHost:
 		inSNAC := oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -54,7 +54,7 @@ func (rt *ICBMRouter) RouteICBM(ctx context.Context, sess *state.Session, SNACFr
 			return nil
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.ICBMEvilRequest:
 		inSNAC := oscar.SNAC_0x04_0x08_ICBMEvilRequest{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -65,7 +65,7 @@ func (rt *ICBMRouter) RouteICBM(ctx context.Context, sess *state.Session, SNACFr
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.ICBMClientErr:
 		inSNAC := oscar.SNAC_0x04_0x0B_ICBMClientErr{}
 		rt.logRequest(ctx, SNACFrame, inSNAC)

+ 5 - 5
server/locate.go

@@ -2,11 +2,11 @@ package server
 
 import (
 	"context"
-	"github.com/mkaminski/goaim/state"
 	"io"
 	"log/slog"
 
 	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
 )
 
 type LocateHandler interface {
@@ -36,7 +36,7 @@ func (rt LocateRouter) RouteLocate(ctx context.Context, sess *state.Session, SNA
 	case oscar.LocateRightsQuery:
 		outSNAC := rt.RightsQueryHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, nil, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.LocateSetInfo:
 		inSNAC := oscar.SNAC_0x02_0x04_LocateSetInfo{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -51,7 +51,7 @@ func (rt LocateRouter) RouteLocate(ctx context.Context, sess *state.Session, SNA
 		}
 		outSNAC := rt.SetDirInfoHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.LocateGetDirInfo:
 		inSNAC := oscar.SNAC_0x02_0x0B_LocateGetDirInfo{}
 		rt.logRequest(ctx, SNACFrame, inSNAC)
@@ -63,7 +63,7 @@ func (rt LocateRouter) RouteLocate(ctx context.Context, sess *state.Session, SNA
 		}
 		outSNAC := rt.SetKeywordInfoHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.LocateUserInfoQuery2:
 		inSNAC := oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -74,7 +74,7 @@ func (rt LocateRouter) RouteLocate(ctx context.Context, sess *state.Session, SNA
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}

+ 2 - 1
server/locate_test.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"bytes"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestLocateRouter_RouteLocate(t *testing.T) {

+ 1 - 1
server/mgmt_api.go

@@ -3,13 +3,13 @@ package server
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/mkaminski/goaim/state"
 	"log/slog"
 	"net"
 	"net/http"
 	"os"
 
 	"github.com/google/uuid"
+	"github.com/mkaminski/goaim/state"
 )
 
 func StartManagementAPI(fs *state.SQLiteFeedbagStore, logger *slog.Logger) {

+ 9 - 21
server/oservice.go

@@ -3,10 +3,11 @@ package server
 import (
 	"context"
 	"errors"
-	"github.com/mkaminski/goaim/oscar"
-	"github.com/mkaminski/goaim/state"
 	"io"
 	"log/slog"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/mkaminski/goaim/state"
 )
 
 type OServiceHandler interface {
@@ -28,7 +29,6 @@ type OServiceBOSHandler interface {
 type OServiceChatHandler interface {
 	OServiceHandler
 	WriteOServiceHostOnline() oscar.XMessage
-	ServiceRequestHandler(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (oscar.XMessage, error)
 	ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *state.Session, chatID string) error
 }
 
@@ -42,7 +42,7 @@ func (rt OServiceRouter) RouteOService(ctx context.Context, sess *state.Session,
 	case oscar.OServiceRateParamsQuery:
 		outSNAC := rt.RateParamsQueryHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, nil, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.OServiceRateParamsSubAdd:
 		inSNAC := oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -54,7 +54,7 @@ func (rt OServiceRouter) RouteOService(ctx context.Context, sess *state.Session,
 	case oscar.OServiceUserInfoQuery:
 		outSNAC := rt.UserInfoQueryHandler(ctx, sess)
 		rt.logRequestAndResponse(ctx, SNACFrame, nil, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.OServiceIdleNotification:
 		inSNAC := oscar.SNAC_0x01_0x11_OServiceIdleNotification{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -69,7 +69,7 @@ func (rt OServiceRouter) RouteOService(ctx context.Context, sess *state.Session,
 		}
 		outSNAC := rt.ClientVersionsHandler(ctx, inSNAC)
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.OServiceSetUserInfoFields:
 		inSNAC := oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -80,7 +80,7 @@ func (rt OServiceRouter) RouteOService(ctx context.Context, sess *state.Session,
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}
@@ -118,7 +118,7 @@ func (rt OServiceBOSRouter) RouteOService(ctx context.Context, sess *state.Sessi
 			return err
 		}
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
 	case oscar.OServiceClientOnline:
 		inSNAC := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -152,19 +152,7 @@ type OServiceChatRouter struct {
 func (rt OServiceChatRouter) RouteOService(ctx context.Context, sess *state.Session, chatID string, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.OServiceServiceRequest:
-		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
-		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
-			return err
-		}
-		outSNAC, err := rt.ServiceRequestHandler(ctx, sess, inSNAC)
-		switch {
-		case errors.Is(err, ErrUnsupportedSubGroup):
-			return sendInvalidSNACErr(SNACFrame, w, sequence)
-		case err != nil:
-			return err
-		}
-		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.SnacFrame, outSNAC.SnacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.SnacFrame, outSNAC.SnacOut, sequence, w)
+		return sendInvalidSNACErr(SNACFrame, w, sequence)
 	case oscar.OServiceClientOnline:
 		inSNAC := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {

+ 0 - 54
server/oservice_chat_mock.go

@@ -232,60 +232,6 @@ func (_c *mockOServiceChatHandler_RateParamsSubAddHandler_Call) RunAndReturn(run
 	return _c
 }
 
-// ServiceRequestHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
-func (_m *mockOServiceChatHandler) ServiceRequestHandler(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (oscar.XMessage, error) {
-	ret := _m.Called(ctx, sess, snacPayloadIn)
-
-	var r0 oscar.XMessage
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (oscar.XMessage, error)); ok {
-		return rf(ctx, sess, snacPayloadIn)
-	}
-	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) oscar.XMessage); ok {
-		r0 = rf(ctx, sess, snacPayloadIn)
-	} else {
-		r0 = ret.Get(0).(oscar.XMessage)
-	}
-
-	if rf, ok := ret.Get(1).(func(context.Context, *state.Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) error); ok {
-		r1 = rf(ctx, sess, snacPayloadIn)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// mockOServiceChatHandler_ServiceRequestHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServiceRequestHandler'
-type mockOServiceChatHandler_ServiceRequestHandler_Call struct {
-	*mock.Call
-}
-
-// ServiceRequestHandler is a helper method to define mock.On call
-//   - ctx context.Context
-//   - sess *state.Session
-//   - snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest
-func (_e *mockOServiceChatHandler_Expecter) ServiceRequestHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *mockOServiceChatHandler_ServiceRequestHandler_Call {
-	return &mockOServiceChatHandler_ServiceRequestHandler_Call{Call: _e.mock.On("ServiceRequestHandler", ctx, sess, snacPayloadIn)}
-}
-
-func (_c *mockOServiceChatHandler_ServiceRequestHandler_Call) Run(run func(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest)) *mockOServiceChatHandler_ServiceRequestHandler_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*state.Session), args[2].(oscar.SNAC_0x01_0x04_OServiceServiceRequest))
-	})
-	return _c
-}
-
-func (_c *mockOServiceChatHandler_ServiceRequestHandler_Call) Return(_a0 oscar.XMessage, _a1 error) *mockOServiceChatHandler_ServiceRequestHandler_Call {
-	_c.Call.Return(_a0, _a1)
-	return _c
-}
-
-func (_c *mockOServiceChatHandler_ServiceRequestHandler_Call) RunAndReturn(run func(context.Context, *state.Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (oscar.XMessage, error)) *mockOServiceChatHandler_ServiceRequestHandler_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // SetUserInfoFieldsHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
 func (_m *mockOServiceChatHandler) SetUserInfoFieldsHandler(ctx context.Context, sess *state.Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (oscar.XMessage, error) {
 	ret := _m.Called(ctx, sess, snacPayloadIn)

+ 6 - 13
server/oservice_test.go

@@ -2,10 +2,11 @@ package server
 
 import (
 	"bytes"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"testing"
 )
 
 func TestOServiceRouter_RouteOService_ForBOS(t *testing.T) {
@@ -338,7 +339,7 @@ func TestOServiceRouter_RouteOService_ForChat(t *testing.T) {
 			output: oscar.XMessage{},
 		},
 		{
-			name: "receive OServiceServiceRequest, return OServiceServiceResponse",
+			name: "receive OServiceServiceRequest, return OServiceErr",
 			input: oscar.XMessage{
 				SnacFrame: oscar.SnacFrame{
 					FoodGroup: oscar.OSERVICE,
@@ -351,14 +352,10 @@ func TestOServiceRouter_RouteOService_ForChat(t *testing.T) {
 			output: oscar.XMessage{
 				SnacFrame: oscar.SnacFrame{
 					FoodGroup: oscar.OSERVICE,
-					SubGroup:  oscar.OServiceServiceResponse,
+					SubGroup:  oscar.OServiceErr,
 				},
-				SnacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
-					TLVRestBlock: oscar.TLVRestBlock{
-						TLVList: oscar.TLVList{
-							oscar.NewTLV(0x01, uint16(1000)),
-						},
-					},
+				SnacOut: oscar.SnacError{
+					Code: oscar.ErrorCodeInvalidSnac,
 				},
 			},
 		},
@@ -536,10 +533,6 @@ func TestOServiceRouter_RouteOService_ForChat(t *testing.T) {
 				Maybe()
 
 			svcBOS := newMockOServiceChatHandler(t)
-			svcBOS.EXPECT().
-				ServiceRequestHandler(mock.Anything, mock.Anything, tc.input.SnacOut).
-				Return(tc.output, tc.handlerErr).
-				Maybe()
 			svcBOS.EXPECT().
 				ClientOnlineHandler(mock.Anything, tc.input.SnacOut, mock.Anything, mock.Anything).
 				Return(tc.handlerErr).

+ 0 - 185
server/router.go

@@ -1,185 +0,0 @@
-package server
-
-import (
-	"bytes"
-	"context"
-	"errors"
-	"github.com/google/uuid"
-	"github.com/mkaminski/goaim/oscar"
-	"github.com/mkaminski/goaim/state"
-	"io"
-	"net"
-)
-
-type AuthHandler interface {
-	ReceiveAndSendAuthChallenge(snacPayloadIn oscar.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (oscar.XMessage, error)
-	ReceiveAndSendBUCPLoginRequest(snacPayloadIn oscar.SNAC_0x17_0x02_BUCPLoginRequest, newUUID func() uuid.UUID) (oscar.XMessage, error)
-	RetrieveChatSession(ctx context.Context, chatID string, sessID string) (*state.Session, error)
-	SendAndReceiveSignonFrame(rw io.ReadWriter, sequence *uint32) (oscar.FlapSignonFrame, error)
-	Signout(ctx context.Context, sess *state.Session) error
-	SignoutChat(ctx context.Context, sess *state.Session, chatID string)
-	VerifyChatLogin(rw io.ReadWriter) (*ChatCookie, uint32, error)
-	VerifyLogin(conn net.Conn) (*state.Session, uint32, error)
-}
-
-type BOSService struct {
-	AlertRouter
-	AuthHandler
-	BuddyRouter
-	ChatNavRouter
-	FeedbagRouter
-	ICBMRouter
-	LocateRouter
-	OServiceBOSRouter
-	Cfg Config
-	RouteLogger
-}
-
-type BOSServiceManager interface {
-	Route(ctx context.Context, sess *state.Session, r io.Reader, w io.Writer, sequence *uint32) error
-	Signout(ctx context.Context, sess *state.Session) error
-	VerifyLogin(conn net.Conn) (*state.Session, uint32, error)
-	WriteOServiceHostOnline() oscar.XMessage
-}
-
-func (rt BOSService) Route(ctx context.Context, sess *state.Session, r io.Reader, w io.Writer, sequence *uint32) error {
-	snac := oscar.SnacFrame{}
-	if err := oscar.Unmarshal(&snac, r); err != nil {
-		return err
-	}
-
-	err := func() error {
-		switch snac.FoodGroup {
-		case oscar.OSERVICE:
-			return rt.RouteOService(ctx, sess, snac, r, w, sequence)
-		case oscar.LOCATE:
-			return rt.RouteLocate(ctx, sess, snac, r, w, sequence)
-		case oscar.BUDDY:
-			return rt.RouteBuddy(ctx, snac, r, w, sequence)
-		case oscar.ICBM:
-			return rt.RouteICBM(ctx, sess, snac, r, w, sequence)
-		case oscar.CHAT_NAV:
-			return rt.RouteChatNav(ctx, sess, snac, r, w, sequence)
-		case oscar.FEEDBAG:
-			return rt.RouteFeedbag(ctx, sess, snac, r, w, sequence)
-		case oscar.BUCP:
-			return routeBUCP(ctx)
-		case oscar.ALERT:
-			return rt.RouteAlert(ctx, snac)
-		default:
-			return ErrUnsupportedSubGroup
-		}
-	}()
-
-	if err != nil {
-		rt.logRequestError(ctx, snac, err)
-		if errors.Is(err, ErrUnsupportedSubGroup) {
-			if err1 := sendInvalidSNACErr(snac, w, sequence); err1 != nil {
-				err = errors.Join(err1, err)
-			}
-			if rt.Cfg.FailFast {
-				panic(err.Error())
-			}
-			return nil
-		}
-	}
-
-	return err
-}
-
-type ChatService struct {
-	AuthHandler
-	ChatRouter
-	Config Config
-	OServiceChatRouter
-	RouteLogger
-}
-
-type ChatServiceManager interface {
-	RetrieveChatSession(ctx context.Context, chatID string, sessID string) (*state.Session, error)
-	Route(ctx context.Context, sess *state.Session, r io.Reader, w io.Writer, sequence *uint32, chatID string) error
-	SignoutChat(ctx context.Context, sess *state.Session, chatID string)
-	VerifyChatLogin(rw io.ReadWriter) (*ChatCookie, uint32, error)
-	WriteOServiceHostOnline() oscar.XMessage
-}
-
-func (rt ChatService) Route(ctx context.Context, sess *state.Session, r io.Reader, w io.Writer, sequence *uint32, chatID string) error {
-	snac := oscar.SnacFrame{}
-	if err := oscar.Unmarshal(&snac, r); err != nil {
-		return err
-	}
-
-	err := func() error {
-		switch snac.FoodGroup {
-		case oscar.OSERVICE:
-			return rt.RouteOService(ctx, sess, chatID, snac, r, w, sequence)
-		case oscar.CHAT:
-			return rt.RouteChat(ctx, sess, chatID, snac, r, w, sequence)
-		default:
-			return ErrUnsupportedSubGroup
-		}
-	}()
-
-	if err != nil {
-		rt.logRequestError(ctx, snac, err)
-		if errors.Is(err, ErrUnsupportedSubGroup) {
-			if err1 := sendInvalidSNACErr(snac, w, sequence); err1 != nil {
-				err = errors.Join(err1, err)
-			}
-			if rt.Config.FailFast {
-				panic(err.Error())
-			}
-			return nil
-		}
-	}
-
-	return err
-}
-
-func writeOutSNAC(originsnac oscar.SnacFrame, snacFrame oscar.SnacFrame, snacOut any, sequence *uint32, w io.Writer) error {
-	if originsnac.RequestID != 0 {
-		snacFrame.RequestID = originsnac.RequestID
-	}
-
-	snacBuf := &bytes.Buffer{}
-	if err := oscar.Marshal(snacFrame, snacBuf); err != nil {
-		return err
-	}
-	if err := oscar.Marshal(snacOut, snacBuf); err != nil {
-		return err
-	}
-
-	flap := oscar.FlapFrame{
-		StartMarker:   42,
-		FrameType:     oscar.FlapFrameData,
-		Sequence:      uint16(*sequence),
-		PayloadLength: uint16(snacBuf.Len()),
-	}
-
-	if err := oscar.Marshal(flap, w); err != nil {
-		return err
-	}
-
-	expectLen := snacBuf.Len()
-	c, err := w.Write(snacBuf.Bytes())
-	if err != nil {
-		return err
-	}
-	if c != expectLen {
-		panic("did not write the expected # of bytes")
-	}
-
-	*sequence++
-	return nil
-}
-
-func sendInvalidSNACErr(snac oscar.SnacFrame, w io.Writer, sequence *uint32) error {
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: snac.FoodGroup,
-		SubGroup:  0x01, // error subgroup for all SNACs
-	}
-	snacPayloadOut := oscar.SnacError{
-		Code: oscar.ErrorCodeInvalidSnac,
-	}
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
-}

+ 12 - 12
state/feedbag_store_test.go

@@ -57,7 +57,7 @@ func TestFeedbagStore(t *testing.T) {
 
 func TestFeedbagDelete(t *testing.T) {
 
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 	const screenName = "sn2day"
 
 	defer func() {
@@ -120,7 +120,7 @@ func TestFeedbagDelete(t *testing.T) {
 
 func TestLastModifiedEmpty(t *testing.T) {
 
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 	const screenName = "sn2day"
 
 	defer func() {
@@ -144,7 +144,7 @@ func TestLastModifiedEmpty(t *testing.T) {
 
 func TestLastModifiedNotEmpty(t *testing.T) {
 
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 	const screenName = "sn2day"
 
 	defer func() {
@@ -180,7 +180,7 @@ func TestLastModifiedNotEmpty(t *testing.T) {
 
 func TestProfile(t *testing.T) {
 
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 	const screenName = "sn2day"
 
 	defer func() {
@@ -242,7 +242,7 @@ func TestProfile(t *testing.T) {
 
 func TestProfileNonExistent(t *testing.T) {
 
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 	const screenName = "sn2day"
 
 	defer func() {
@@ -263,7 +263,7 @@ func TestProfileNonExistent(t *testing.T) {
 }
 
 func TestInterestedUsers(t *testing.T) {
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 
 	defer func() {
 		err := os.Remove(testFile)
@@ -293,7 +293,7 @@ func TestInterestedUsers(t *testing.T) {
 }
 
 func TestFeedbagStoreBuddiesBlockedUser(t *testing.T) {
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 
 	defer func() {
 		err := os.Remove(testFile)
@@ -323,7 +323,7 @@ func TestFeedbagStoreBuddiesBlockedUser(t *testing.T) {
 }
 
 func TestFeedbagStoreBlockedA(t *testing.T) {
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 
 	defer func() {
 		err := os.Remove(testFile)
@@ -353,7 +353,7 @@ func TestFeedbagStoreBlockedA(t *testing.T) {
 }
 
 func TestFeedbagStoreBlockedB(t *testing.T) {
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 
 	defer func() {
 		err := os.Remove(testFile)
@@ -383,7 +383,7 @@ func TestFeedbagStoreBlockedB(t *testing.T) {
 }
 
 func TestFeedbagStoreBlockedNoBlocked(t *testing.T) {
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 
 	defer func() {
 		err := os.Remove(testFile)
@@ -412,7 +412,7 @@ func TestFeedbagStoreBlockedNoBlocked(t *testing.T) {
 }
 
 func TestGetUser(t *testing.T) {
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 
 	defer func() {
 		err := os.Remove(testFile)
@@ -448,7 +448,7 @@ func TestGetUser(t *testing.T) {
 }
 
 func TestGetUserNotFound(t *testing.T) {
-	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const testFile string = "aim_test.db"
 
 	defer func() {
 		err := os.Remove(testFile)

+ 3 - 2
state/session.go

@@ -1,10 +1,11 @@
 package state
 
 import (
-	"github.com/google/uuid"
-	"github.com/mkaminski/goaim/oscar"
 	"sync"
 	"time"
+
+	"github.com/google/uuid"
+	"github.com/mkaminski/goaim/oscar"
 )
 
 var CapChat, _ = uuid.MustParse("748F2420-6287-11D1-8222-444553540000").MarshalBinary()

+ 3 - 2
state/session_test.go

@@ -1,10 +1,11 @@
 package state
 
 import (
-	"github.com/mkaminski/goaim/oscar"
-	"github.com/stretchr/testify/assert"
 	"testing"
 	"time"
+
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestSession_SendMessage_SessSendOK(t *testing.T) {