From 76f98338d8876f147e5e7c7caaff88507694e9ad Mon Sep 17 00:00:00 2001 From: Misha Nasledov Date: Fri, 9 Oct 2026 16:38:19 -0700 Subject: [PATCH] Handle an Agreed transaction without the options field HandleTranAgreed read field 113 as a uint16 without checking it was there, so an Agreed sent without it panicked. The per-transaction recover keeps the connection, but the login stops short: no reply, no NotifyChangeUser to the other users, and no banner. Treat a missing or short options field as no options set, as HandleSetClientUserInfo already does for clients that don't send it. --- internal/mobius/handlers_session.go | 8 +++- internal/mobius/handlers_session_test.go | 56 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/internal/mobius/handlers_session.go b/internal/mobius/handlers_session.go index 836c608..2fa64c4 100644 --- a/internal/mobius/handlers_session.go +++ b/internal/mobius/handlers_session.go @@ -131,8 +131,12 @@ func HandleTranAgreed(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot cc.Logger = cc.Logger.With("Name", string(cc.GetUserName())) cc.Logger.Info("Login successful") - options := t.GetField(hotline.FieldOptions).Data - optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options))) + // the options field is only passed by the client versions > 1.2.3. + var opts uint16 + if options := t.GetField(hotline.FieldOptions).Data; len(options) >= 2 { + opts = binary.BigEndian.Uint16(options) + } + optBitmap := big.NewInt(int64(opts)) // Check refuse private PM option cc.SetFlag(hotline.UserFlagRefusePM, optBitmap.Bit(hotline.UserOptRefusePM)) diff --git a/internal/mobius/handlers_session_test.go b/internal/mobius/handlers_session_test.go index ea96def..0398d0d 100644 --- a/internal/mobius/handlers_session_test.go +++ b/internal/mobius/handlers_session_test.go @@ -497,6 +497,62 @@ func TestHandleTranAgreed(t *testing.T) { }, }, }, + { + name: "without options field", + args: args{ + cc: &hotline.ClientConn{ + Account: &hotline.Account{ + Access: func() hotline.AccessBitmap { + var bits hotline.AccessBitmap + bits.Set(hotline.AccessDisconUser) + bits.Set(hotline.AccessAnyName) + return bits + }()}, + Icon: []byte{0, 1}, + Flags: [2]byte{0, 1}, + Version: []byte{0, 1}, + ID: [2]byte{0, 1}, + Logger: NewTestLogger(), + Server: &hotline.Server{ + TextDecoder: charmap.Macintosh.NewDecoder(), + TextEncoder: charmap.Macintosh.NewEncoder(), + Config: hotline.Config{ + BannerFile: "Banner.jpg", + }, + ClientMgr: func() *hltest.MockClientMgr { + m := hltest.MockClientMgr{} + m.On("List").Return([]*hotline.ClientConn{ + //{ + // ID: [2]byte{0, 2}, + // UserName: []byte("UserB"), + //}, + }, + ) + return &m + }(), + }, + }, + t: hotline.NewTransaction( + hotline.TranAgreed, [2]byte{}, + hotline.NewField(hotline.FieldUserName, []byte("username")), + hotline.NewField(hotline.FieldUserIconID, []byte{0, 1}), + ), + }, + wantRes: []hotline.Transaction{ + { + ClientID: [2]byte{0, 1}, + Type: [2]byte{0, 0x7a}, + Fields: []hotline.Field{ + hotline.NewField(hotline.FieldBannerType, []byte("JPEG")), + }, + }, + { + ClientID: [2]byte{0, 1}, + IsReply: 0x01, + Fields: []hotline.Field{}, + }, + }, + }, { name: "with gif banner", args: args{