X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/blobdiff_plain/a55350daaf83498b7a237c027ad0dd2377f06fee..a2ef262a164fc735b9b8471ac0c8001eea2b9bf6:/hotline/transaction.go?ds=sidebyside diff --git a/hotline/transaction.go b/hotline/transaction.go index 0caf72f..f8c7dfd 100644 --- a/hotline/transaction.go +++ b/hotline/transaction.go @@ -7,119 +7,189 @@ import ( "errors" "fmt" "io" + "log/slog" "math/rand" "slices" ) -const ( - TranError = 0 - TranGetMsgs = 101 - TranNewMsg = 102 - TranOldPostNews = 103 - TranServerMsg = 104 - TranChatSend = 105 - TranChatMsg = 106 - TranLogin = 107 - TranSendInstantMsg = 108 - TranShowAgreement = 109 - TranDisconnectUser = 110 - TranDisconnectMsg = 111 // TODO: implement server initiated friendly disconnect - TranInviteNewChat = 112 - TranInviteToChat = 113 - TranRejectChatInvite = 114 - TranJoinChat = 115 - TranLeaveChat = 116 - TranNotifyChatChangeUser = 117 - TranNotifyChatDeleteUser = 118 - TranNotifyChatSubject = 119 - TranSetChatSubject = 120 - TranAgreed = 121 - TranServerBanner = 122 - TranGetFileNameList = 200 - TranDownloadFile = 202 - TranUploadFile = 203 - TranNewFolder = 205 - TranDeleteFile = 204 - TranGetFileInfo = 206 - TranSetFileInfo = 207 - TranMoveFile = 208 - TranMakeFileAlias = 209 - TranDownloadFldr = 210 - TranDownloadInfo = 211 // TODO: implement file transfer queue - TranDownloadBanner = 212 - TranUploadFldr = 213 - TranGetUserNameList = 300 - TranNotifyChangeUser = 301 - TranNotifyDeleteUser = 302 - TranGetClientInfoText = 303 - TranSetClientUserInfo = 304 - TranListUsers = 348 - TranUpdateUser = 349 - TranNewUser = 350 - TranDeleteUser = 351 - TranGetUser = 352 - TranSetUser = 353 - TranUserAccess = 354 - TranUserBroadcast = 355 - TranGetNewsCatNameList = 370 - TranGetNewsArtNameList = 371 - TranDelNewsItem = 380 - TranNewNewsFldr = 381 - TranNewNewsCat = 382 - TranGetNewsArtData = 400 - TranPostNewsArt = 410 - TranDelNewsArt = 411 - TranKeepAlive = 500 +var ( + TranError = [2]byte{0x00, 0x00} // 0 + TranGetMsgs = [2]byte{0x00, 0x65} // 101 + TranNewMsg = [2]byte{0x00, 0x66} // 102 + TranOldPostNews = [2]byte{0x00, 0x67} // 103 + TranServerMsg = [2]byte{0x00, 0x68} // 104 + TranChatSend = [2]byte{0x00, 0x69} // 105 + TranChatMsg = [2]byte{0x00, 0x6A} // 106 + TranLogin = [2]byte{0x00, 0x6B} // 107 + TranSendInstantMsg = [2]byte{0x00, 0x6C} // 108 + TranShowAgreement = [2]byte{0x00, 0x6D} // 109 + TranDisconnectUser = [2]byte{0x00, 0x6E} // 110 + TranDisconnectMsg = [2]byte{0x00, 0x6F} // 111 + TranInviteNewChat = [2]byte{0x00, 0x70} // 112 + TranInviteToChat = [2]byte{0x00, 0x71} // 113 + TranRejectChatInvite = [2]byte{0x00, 0x72} // 114 + TranJoinChat = [2]byte{0x00, 0x73} // 115 + TranLeaveChat = [2]byte{0x00, 0x74} // 116 + TranNotifyChatChangeUser = [2]byte{0x00, 0x75} // 117 + TranNotifyChatDeleteUser = [2]byte{0x00, 0x76} // 118 + TranNotifyChatSubject = [2]byte{0x00, 0x77} // 119 + TranSetChatSubject = [2]byte{0x00, 0x78} // 120 + TranAgreed = [2]byte{0x00, 0x79} // 121 + TranServerBanner = [2]byte{0x00, 0x7A} // 122 + TranGetFileNameList = [2]byte{0x00, 0xC8} // 200 + TranDownloadFile = [2]byte{0x00, 0xCA} // 202 + TranUploadFile = [2]byte{0x00, 0xCB} // 203 + TranNewFolder = [2]byte{0x00, 0xCD} // 205 + TranDeleteFile = [2]byte{0x00, 0xCC} // 204 + TranGetFileInfo = [2]byte{0x00, 0xCE} // 206 + TranSetFileInfo = [2]byte{0x00, 0xCF} // 207 + TranMoveFile = [2]byte{0x00, 0xD0} // 208 + TranMakeFileAlias = [2]byte{0x00, 0xD1} // 209 + TranDownloadFldr = [2]byte{0x00, 0xD2} // 210 + TranDownloadInfo = [2]byte{0x00, 0xD3} // 211 + TranDownloadBanner = [2]byte{0x00, 0xD4} // 212 + TranUploadFldr = [2]byte{0x00, 0xD5} // 213 + TranGetUserNameList = [2]byte{0x01, 0x2C} // 300 + TranNotifyChangeUser = [2]byte{0x01, 0x2D} // 301 + TranNotifyDeleteUser = [2]byte{0x01, 0x2E} // 302 + TranGetClientInfoText = [2]byte{0x01, 0x2F} // 303 + TranSetClientUserInfo = [2]byte{0x01, 0x30} // 304 + TranListUsers = [2]byte{0x01, 0x5C} // 348 + TranUpdateUser = [2]byte{0x01, 0x5D} // 349 + TranNewUser = [2]byte{0x01, 0x5E} // 350 + TranDeleteUser = [2]byte{0x01, 0x5F} // 351 + TranGetUser = [2]byte{0x01, 0x60} // 352 + TranSetUser = [2]byte{0x01, 0x61} // 353 + TranUserAccess = [2]byte{0x01, 0x62} // 354 + TranUserBroadcast = [2]byte{0x01, 0x63} // 355 + TranGetNewsCatNameList = [2]byte{0x01, 0x72} // 370 + TranGetNewsArtNameList = [2]byte{0x01, 0x73} // 371 + TranDelNewsItem = [2]byte{0x01, 0x7C} // 380 + TranNewNewsFldr = [2]byte{0x01, 0x7D} // 381 + TranNewNewsCat = [2]byte{0x01, 0x7E} // 382 + TranGetNewsArtData = [2]byte{0x01, 0x90} // 400 + TranPostNewsArt = [2]byte{0x01, 0x9A} // 410 + TranDelNewsArt = [2]byte{0x01, 0x9B} // 411 + TranKeepAlive = [2]byte{0x01, 0xF4} // 500 ) type Transaction struct { - Flags byte // Reserved (should be 0) - IsReply byte // Request (0) or reply (1) - Type [2]byte // Requested operation (user defined) - ID [4]byte // Unique transaction ID (must be != 0) - ErrorCode [4]byte // Used in the reply (user defined, 0 = no error) - TotalSize [4]byte // Total data size for the transaction (all parts) - DataSize [4]byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts. - ParamCount [2]byte // Number of the parameters for this transaction + Flags byte // Reserved (should be 0) + IsReply byte // Request (0) or reply (1) + Type TranType // Requested operation (user defined) + ID [4]byte // Unique transaction ID (must be != 0) + ErrorCode [4]byte // Used in the reply (user defined, 0 = no error) + TotalSize [4]byte // Total data size for the fields in this transaction. + DataSize [4]byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts. + ParamCount [2]byte // Number of the parameters for this transaction Fields []Field - clientID *[]byte // Internal identifier for target client + clientID [2]byte // Internal identifier for target client readOffset int // Internal offset to track read progress } -func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction { - typeSlice := make([]byte, 2) - binary.BigEndian.PutUint16(typeSlice, uint16(t)) +type TranType [2]byte + +var tranTypeNames = map[TranType]string{ + TranChatMsg: "Receive Chat", + TranNotifyChangeUser: "TranNotifyChangeUser", + TranError: "TranError", + TranShowAgreement: "TranShowAgreement", + TranUserAccess: "TranUserAccess", + TranNotifyDeleteUser: "TranNotifyDeleteUser", + TranAgreed: "TranAgreed", + TranChatSend: "Send Chat", + TranDelNewsArt: "TranDelNewsArt", + TranDelNewsItem: "TranDelNewsItem", + TranDeleteFile: "TranDeleteFile", + TranDeleteUser: "TranDeleteUser", + TranDisconnectUser: "TranDisconnectUser", + TranDownloadFile: "TranDownloadFile", + TranDownloadFldr: "TranDownloadFldr", + TranGetClientInfoText: "TranGetClientInfoText", + TranGetFileInfo: "TranGetFileInfo", + TranGetFileNameList: "TranGetFileNameList", + TranGetMsgs: "TranGetMsgs", + TranGetNewsArtData: "TranGetNewsArtData", + TranGetNewsArtNameList: "TranGetNewsArtNameList", + TranGetNewsCatNameList: "TranGetNewsCatNameList", + TranGetUser: "TranGetUser", + TranGetUserNameList: "tranHandleGetUserNameList", + TranInviteNewChat: "TranInviteNewChat", + TranInviteToChat: "TranInviteToChat", + TranJoinChat: "TranJoinChat", + TranKeepAlive: "TranKeepAlive", + TranLeaveChat: "TranJoinChat", + TranListUsers: "TranListUsers", + TranMoveFile: "TranMoveFile", + TranNewFolder: "TranNewFolder", + TranNewNewsCat: "TranNewNewsCat", + TranNewNewsFldr: "TranNewNewsFldr", + TranNewUser: "TranNewUser", + TranUpdateUser: "TranUpdateUser", + TranOldPostNews: "TranOldPostNews", + TranPostNewsArt: "TranPostNewsArt", + TranRejectChatInvite: "TranRejectChatInvite", + TranSendInstantMsg: "TranSendInstantMsg", + TranSetChatSubject: "TranSetChatSubject", + TranMakeFileAlias: "TranMakeFileAlias", + TranSetClientUserInfo: "TranSetClientUserInfo", + TranSetFileInfo: "TranSetFileInfo", + TranSetUser: "TranSetUser", + TranUploadFile: "TranUploadFile", + TranUploadFldr: "TranUploadFldr", + TranUserBroadcast: "TranUserBroadcast", + TranDownloadBanner: "TranDownloadBanner", +} - idSlice := make([]byte, 4) - binary.BigEndian.PutUint32(idSlice, rand.Uint32()) +func (t TranType) LogValue() slog.Value { + return slog.StringValue(tranTypeNames[t]) +} - return &Transaction{ +// NewTransaction creates a new Transaction with the specified type, client ID, and optional fields. +func NewTransaction(t, clientID [2]byte, fields ...Field) Transaction { + transaction := Transaction{ + Type: t, clientID: clientID, - Type: [2]byte(typeSlice), - ID: [4]byte(idSlice), Fields: fields, } + + binary.BigEndian.PutUint32(transaction.ID[:], rand.Uint32()) + + return transaction } -// Write implements io.Writer interface for Transaction +// Write implements io.Writer interface for Transaction. +// Transactions read from the network are read as complete tokens with a bufio.Scanner, so +// the arg p is guaranteed to have the full byte payload of a complete transaction. func (t *Transaction) Write(p []byte) (n int, err error) { - totalSize := binary.BigEndian.Uint32(p[12:16]) + // Make sure we have the minimum number of bytes for a transaction. + if len(p) < 22 { + return 0, errors.New("buffer too small") + } - // the buf may include extra bytes that are not part of the transaction - // tranLen represents the length of bytes that are part of the transaction + // Read the total size field. + totalSize := binary.BigEndian.Uint32(p[12:16]) tranLen := int(20 + totalSize) - if tranLen > len(p) { - return n, errors.New("buflen too small for tranLen") - } + paramCount := binary.BigEndian.Uint16(p[20:22]) + + t.Flags = p[0] + t.IsReply = p[1] + copy(t.Type[:], p[2:4]) + copy(t.ID[:], p[4:8]) + copy(t.ErrorCode[:], p[8:12]) + copy(t.TotalSize[:], p[12:16]) + copy(t.DataSize[:], p[16:20]) + copy(t.ParamCount[:], p[20:22]) - // Create a new scanner for parsing incoming bytes into transaction tokens scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen])) scanner.Split(fieldScanner) - for i := 0; i < int(binary.BigEndian.Uint16(p[20:22])); i++ { - scanner.Scan() + for i := 0; i < int(paramCount); i++ { + if !scanner.Scan() { + return 0, fmt.Errorf("error scanning field: %w", scanner.Err()) + } var field Field if _, err := field.Write(scanner.Bytes()); err != nil { @@ -128,16 +198,11 @@ func (t *Transaction) Write(p []byte) (n int, err error) { t.Fields = append(t.Fields, field) } - t.Flags = p[0] - t.IsReply = p[1] - t.Type = [2]byte(p[2:4]) - t.ID = [4]byte(p[4:8]) - t.ErrorCode = [4]byte(p[8:12]) - t.TotalSize = [4]byte(p[12:16]) - t.DataSize = [4]byte(p[16:20]) - t.ParamCount = [2]byte(p[20:22]) - - return len(p), err + if err := scanner.Err(); err != nil { + return 0, fmt.Errorf("scanner error: %w", err) + } + + return len(p), nil } const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields @@ -253,16 +318,12 @@ func (t *Transaction) Size() []byte { return bs } -func (t *Transaction) GetField(id int) Field { +func (t *Transaction) GetField(id [2]byte) Field { for _, field := range t.Fields { - if id == int(binary.BigEndian.Uint16(field.ID[:])) { + if id == field.ID { return field } } return Field{} } - -func (t *Transaction) IsError() bool { - return t.ErrorCode == [4]byte{0, 0, 0, 1} -}