From: Jeff Halter Date: Wed, 18 Jan 2023 21:52:52 +0000 (-0800) Subject: Fix threaded news bugs X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/commitdiff_plain/3326539367dfa85cb2b0455d5b1c67eaaf59cf8e?ds=sidebyside Fix threaded news bugs 1. Fix handling of article IDs sent as 4 bytes instead of 2 2. Fix incorrect article count listing that strangely only affects third party clients --- diff --git a/hotline/news.go b/hotline/news.go index e25a2c8..11e2a27 100644 --- a/hotline/news.go +++ b/hotline/news.go @@ -52,6 +52,7 @@ func (newscat *NewsCategoryListData15) GetNewsArtListData() NewsArtListData { nald := NewsArtListData{ ID: []byte{0, 0, 0, 0}, + Count: len(newsArts), Name: []byte{}, Description: []byte{}, NewsArtList: newsArtsPayload, @@ -85,11 +86,12 @@ type NewsArtListData struct { Name []byte `yaml:"Name"` Description []byte `yaml:"Description"` // not used? NewsArtList []byte // List of articles Optional (if article count > 0) + Count int } func (nald *NewsArtListData) Payload() []byte { count := make([]byte, 4) - binary.BigEndian.PutUint32(count, uint32(len(nald.NewsArtList))) + binary.BigEndian.PutUint32(count, uint32(nald.Count)) out := append(nald.ID, count...) out = append(out, []byte{uint8(len(nald.Name))}...) diff --git a/hotline/server.go b/hotline/server.go index bda0c2a..d87ddb9 100644 --- a/hotline/server.go +++ b/hotline/server.go @@ -36,6 +36,8 @@ type requestCtx struct { var nostalgiaVersion = []byte{0, 0, 2, 0x2c} // version ID used by the Nostalgia client var frogblastVersion = []byte{0, 0, 0, 0xb9} // version ID used by the Frogblast 1.2.4 client +var heildrun = []byte{0, 0x97} + var obsessionVersion = []byte{0xbe, 0x00} // version ID used by the Obsession client type Server struct { @@ -676,7 +678,7 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser // Used simplified hotline v1.2.3 login flow for clients that do not send login info in tranAgreed // TODO: figure out a generalized solution that doesn't require playing whack-a-mole for specific client versions - if c.Version == nil || bytes.Equal(c.Version, nostalgiaVersion) || bytes.Equal(c.Version, frogblastVersion) || bytes.Equal(c.Version, obsessionVersion) { + if c.Version == nil || bytes.Equal(c.Version, nostalgiaVersion) || bytes.Equal(c.Version, frogblastVersion) || bytes.Equal(c.Version, obsessionVersion) || bytes.Equal(c.Version, heildrun) { c.Agreed = true c.logger = c.logger.With("name", string(c.UserName)) c.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(c.Version); return i }())) diff --git a/hotline/transaction_handlers.go b/hotline/transaction_handlers.go index 7f8cbef..12b1d41 100644 --- a/hotline/transaction_handlers.go +++ b/hotline/transaction_handlers.go @@ -287,6 +287,7 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro // HandleSendInstantMsg sends instant message to the user on the current server. // Fields used in the request: +// // 103 User ID // 113 Options // One of the following values: @@ -897,17 +898,6 @@ func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err return res, err } -func byteToInt(bytes []byte) (int, error) { - switch len(bytes) { - case 2: - return int(binary.BigEndian.Uint16(bytes)), nil - case 4: - return int(binary.BigEndian.Uint32(bytes)), nil - } - - return 0, errors.New("unknown byte length") -} - // HandleGetClientInfoText returns user information for the specific user. // // Fields used in the request: @@ -1198,10 +1188,12 @@ func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err e return res, err } +// HandleGetNewsArtData gets the list of article names at the specified news path. + // Fields used in the request: // 325 News path Optional -// -// Reply fields: + +// Fields used in the reply: // 321 News article list data Optional func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) { if !cc.Authorize(accessNewsReadArt) { @@ -1224,47 +1216,51 @@ func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction return res, err } +// HandleGetNewsArtData requests information about the specific news article. +// Fields used in the request: +// +// Request fields +// 325 News path +// 326 News article ID +// 327 News article data flavor +// +// Fields used in the reply: +// 328 News article title +// 329 News article poster +// 330 News article date +// 331 Previous article ID +// 332 Next article ID +// 335 Parent article ID +// 336 First child article ID +// 327 News article data flavor "Should be “text/plain” +// 333 News article data Optional (if data flavor is “text/plain”) func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) { if !cc.Authorize(accessNewsReadArt) { res = append(res, cc.NewErrReply(t, "You are not allowed to read news.")) return res, err } - // Request fields - // 325 News fp - // 326 News article ID - // 327 News article data flavor - - pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data) - var cat NewsCategoryListData15 cats := cc.Server.ThreadedNews.Categories - for _, fp := range pathStrs { + for _, fp := range ReadNewsPath(t.GetField(fieldNewsPath).Data) { cat = cats[fp] cats = cats[fp].SubCats } - newsArtID := t.GetField(fieldNewsArtID).Data - convertedArtID := binary.BigEndian.Uint16(newsArtID) + // The official Hotline clients will send the article ID as 2 bytes if possible, but + // some third party clients such as Frogblast and Heildrun will always send 4 bytes + convertedID, err := byteToInt(t.GetField(fieldNewsArtID).Data) + if err != nil { + return res, err + } - art := cat.Articles[uint32(convertedArtID)] + art := cat.Articles[uint32(convertedID)] if art == nil { res = append(res, cc.NewReply(t)) return res, err } - // Reply fields - // 328 News article title - // 329 News article poster - // 330 News article date - // 331 Previous article ID - // 332 Next article ID - // 335 Parent article ID - // 336 First child article ID - // 327 News article data flavor "Should be “text/plain” - // 333 News article data Optional (if data flavor is “text/plain”) - res = append(res, cc.NewReply(t, NewField(fieldNewsArtTitle, []byte(art.Title)), NewField(fieldNewsArtPoster, []byte(art.Poster)), @@ -1882,7 +1878,8 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err erro // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat // Fields used in the request: -// * 114 fieldChatID +// - 114 fieldChatID +// // Reply is not expected. func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) { chatID := t.GetField(fieldChatID).Data diff --git a/hotline/transaction_handlers_test.go b/hotline/transaction_handlers_test.go index 4dee0b5..2720cdd 100644 --- a/hotline/transaction_handlers_test.go +++ b/hotline/transaction_handlers_test.go @@ -3684,3 +3684,58 @@ func TestHandleInviteNewChat(t *testing.T) { }) } } + +func TestHandleGetNewsArtData(t *testing.T) { + type args struct { + cc *ClientConn + t *Transaction + } + tests := []struct { + name string + args args + wantRes []Transaction + wantErr assert.ErrorAssertionFunc + }{ + { + name: "when user does not have required permission", + args: args{ + cc: &ClientConn{ + Account: &Account{ + Access: func() accessBitmap { + var bits accessBitmap + return bits + }(), + }, + Server: &Server{ + Accounts: map[string]*Account{}, + }, + }, + t: NewTransaction( + tranGetNewsArtData, &[]byte{0, 1}, + ), + }, + wantRes: []Transaction{ + { + Flags: 0x00, + IsReply: 0x01, + Type: []byte{0, 0x00}, + ID: []byte{0x9a, 0xcb, 0x04, 0x42}, + ErrorCode: []byte{0, 0, 0, 1}, + Fields: []Field{ + NewField(fieldError, []byte("You are not allowed to read news.")), + }, + }, + }, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRes, err := HandleGetNewsArtData(tt.args.cc, tt.args.t) + if !tt.wantErr(t, err, fmt.Sprintf("HandleGetNewsArtData(%v, %v)", tt.args.cc, tt.args.t)) { + return + } + tranAssertEqual(t, tt.wantRes, gotRes) + }) + } +} diff --git a/hotline/util.go b/hotline/util.go new file mode 100644 index 0000000..d37feb5 --- /dev/null +++ b/hotline/util.go @@ -0,0 +1,17 @@ +package hotline + +import ( + "encoding/binary" + "errors" +) + +func byteToInt(bytes []byte) (int, error) { + switch len(bytes) { + case 2: + return int(binary.BigEndian.Uint16(bytes)), nil + case 4: + return int(binary.BigEndian.Uint32(bytes)), nil + } + + return 0, errors.New("unknown byte length") +} diff --git a/hotline/util_test.go b/hotline/util_test.go new file mode 100644 index 0000000..30ae2b6 --- /dev/null +++ b/hotline/util_test.go @@ -0,0 +1,47 @@ +package hotline + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_byteToInt(t *testing.T) { + type args struct { + bytes []byte + } + tests := []struct { + name string + args args + want int + wantErr assert.ErrorAssertionFunc + }{ + { + name: "with 2 bytes of input", + args: args{bytes: []byte{0, 1}}, + want: 1, + wantErr: assert.NoError, + }, + { + name: "with 4 bytes of input", + args: args{bytes: []byte{0, 1, 0, 0}}, + want: 65536, + wantErr: assert.NoError, + }, + { + name: "with invalid number of bytes of input", + args: args{bytes: []byte{1, 0, 0, 0, 0, 0, 0, 0}}, + want: 0, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := byteToInt(tt.args.bytes) + if !tt.wantErr(t, err, fmt.Sprintf("byteToInt(%v)", tt.args.bytes)) { + return + } + assert.Equalf(t, tt.want, got, "byteToInt(%v)", tt.args.bytes) + }) + } +}