X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/blobdiff_plain/153e2eac3b51a6a426556752fa2c532cfe53f026..fd740bc499ebc6d3a381479316f74cdc736d02de:/hotline/transaction.go diff --git a/hotline/transaction.go b/hotline/transaction.go index 0caf72f..fa1e96d 100644 --- a/hotline/transaction.go +++ b/hotline/transaction.go @@ -11,115 +11,181 @@ import ( "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 +type TranType [2]byte + +var ( + TranError = TranType{0x00, 0x00} // 0 + TranGetMsgs = TranType{0x00, 0x65} // 101 + TranNewMsg = TranType{0x00, 0x66} // 102 + TranOldPostNews = TranType{0x00, 0x67} // 103 + TranServerMsg = TranType{0x00, 0x68} // 104 + TranChatSend = TranType{0x00, 0x69} // 105 + TranChatMsg = TranType{0x00, 0x6A} // 106 + TranLogin = TranType{0x00, 0x6B} // 107 + TranSendInstantMsg = TranType{0x00, 0x6C} // 108 + TranShowAgreement = TranType{0x00, 0x6D} // 109 + TranDisconnectUser = TranType{0x00, 0x6E} // 110 + TranDisconnectMsg = TranType{0x00, 0x6F} // 111 + TranInviteNewChat = TranType{0x00, 0x70} // 112 + TranInviteToChat = TranType{0x00, 0x71} // 113 + TranRejectChatInvite = TranType{0x00, 0x72} // 114 + TranJoinChat = TranType{0x00, 0x73} // 115 + TranLeaveChat = TranType{0x00, 0x74} // 116 + TranNotifyChatChangeUser = TranType{0x00, 0x75} // 117 + TranNotifyChatDeleteUser = TranType{0x00, 0x76} // 118 + TranNotifyChatSubject = TranType{0x00, 0x77} // 119 + TranSetChatSubject = TranType{0x00, 0x78} // 120 + TranAgreed = TranType{0x00, 0x79} // 121 + TranServerBanner = TranType{0x00, 0x7A} // 122 + TranGetFileNameList = TranType{0x00, 0xC8} // 200 + TranDownloadFile = TranType{0x00, 0xCA} // 202 + TranUploadFile = TranType{0x00, 0xCB} // 203 + TranNewFolder = TranType{0x00, 0xCD} // 205 + TranDeleteFile = TranType{0x00, 0xCC} // 204 + TranGetFileInfo = TranType{0x00, 0xCE} // 206 + TranSetFileInfo = TranType{0x00, 0xCF} // 207 + TranMoveFile = TranType{0x00, 0xD0} // 208 + TranMakeFileAlias = TranType{0x00, 0xD1} // 209 + TranDownloadFldr = TranType{0x00, 0xD2} // 210 + TranDownloadInfo = TranType{0x00, 0xD3} // 211 + TranDownloadBanner = TranType{0x00, 0xD4} // 212 + TranUploadFldr = TranType{0x00, 0xD5} // 213 + TranGetUserNameList = TranType{0x01, 0x2C} // 300 + TranNotifyChangeUser = TranType{0x01, 0x2D} // 301 + TranNotifyDeleteUser = TranType{0x01, 0x2E} // 302 + TranGetClientInfoText = TranType{0x01, 0x2F} // 303 + TranSetClientUserInfo = TranType{0x01, 0x30} // 304 + TranListUsers = TranType{0x01, 0x5C} // 348 + TranUpdateUser = TranType{0x01, 0x5D} // 349 + TranNewUser = TranType{0x01, 0x5E} // 350 + TranDeleteUser = TranType{0x01, 0x5F} // 351 + TranGetUser = TranType{0x01, 0x60} // 352 + TranSetUser = TranType{0x01, 0x61} // 353 + TranUserAccess = TranType{0x01, 0x62} // 354 + TranUserBroadcast = TranType{0x01, 0x63} // 355 + TranGetNewsCatNameList = TranType{0x01, 0x72} // 370 + TranGetNewsArtNameList = TranType{0x01, 0x73} // 371 + TranDelNewsItem = TranType{0x01, 0x7C} // 380 + TranNewNewsFldr = TranType{0x01, 0x7D} // 381 + TranNewNewsCat = TranType{0x01, 0x7E} // 382 + TranGetNewsArtData = TranType{0x01, 0x90} // 400 + TranPostNewsArt = TranType{0x01, 0x9A} // 410 + TranDelNewsArt = TranType{0x01, 0x9B} // 411 + TranKeepAlive = TranType{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 - readOffset int // Internal offset to track read progress + ClientID ClientID // 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)) - - idSlice := make([]byte, 4) - binary.BigEndian.PutUint32(idSlice, rand.Uint32()) +var tranTypeNames = map[TranType]string{ + TranChatMsg: "Receive chat", + TranNotifyChangeUser: "User change", + TranError: "Error", + TranShowAgreement: "Show agreement", + TranUserAccess: "User access", + TranNotifyDeleteUser: "User left", + TranAgreed: "Accept agreement", + TranChatSend: "Send chat", + TranDelNewsArt: "Delete news article", + TranDelNewsItem: "Delete news item", + TranDeleteFile: "Delete file", + TranDeleteUser: "Delete user", + TranDisconnectUser: "Disconnect user", + TranDownloadFile: "Download file", + TranDownloadFldr: "Download folder", + TranGetClientInfoText: "Get client info", + TranGetFileInfo: "Get file info", + TranGetFileNameList: "Get file list", + TranGetMsgs: "Get messages", + TranGetNewsArtData: "Get news article", + TranGetNewsArtNameList: "Get news article list", + TranGetNewsCatNameList: "Get news categories", + TranGetUser: "Get user", + TranGetUserNameList: "Get user list", + TranInviteNewChat: "Invite to new chat", + TranInviteToChat: "Invite to chat", + TranJoinChat: "Join chat", + TranKeepAlive: "Keepalive", + TranLeaveChat: "Leave chat", + TranListUsers: "List user accounts", + TranMoveFile: "Move file", + TranNewFolder: "Create folder", + TranNewNewsCat: "Create news category", + TranNewNewsFldr: "Create news bundle", + TranNewUser: "Create user account", + TranUpdateUser: "Update user account", + TranOldPostNews: "Post to message board", + TranPostNewsArt: "Create news article", + TranRejectChatInvite: "Decline chat invite", + TranSendInstantMsg: "Send message", + TranSetChatSubject: "Set chat subject", + TranMakeFileAlias: "Make file alias", + TranSetClientUserInfo: "Set client user info", + TranSetFileInfo: "Set file info", + TranSetUser: "Set user", + TranUploadFile: "Upload file", + TranUploadFldr: "Upload folder", + TranUserBroadcast: "Send broadcast", + TranDownloadBanner: "Download banner", +} - return &Transaction{ - clientID: clientID, - Type: [2]byte(typeSlice), - ID: [4]byte(idSlice), +// NewTransaction creates a new Transaction with the specified type, client, and optional fields. +func NewTransaction(t TranType, clientID ClientID, fields ...Field) Transaction { + transaction := Transaction{ + Type: t, + ClientID: clientID, Fields: fields, } + + // Give the transaction a random ID. + 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) + 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 +194,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 @@ -162,45 +223,6 @@ func transactionScanner(data []byte, _ bool) (advance int, token []byte, err err const minFieldLen = 4 -func ReadFields(paramCount []byte, buf []byte) ([]Field, error) { - paramCountInt := int(binary.BigEndian.Uint16(paramCount)) - if paramCountInt > 0 && len(buf) < minFieldLen { - return []Field{}, fmt.Errorf("invalid field length %v", len(buf)) - } - - // A Field consists of: - // ID: 2 bytes - // Size: 2 bytes - // Data: FieldSize number of bytes - var fields []Field - for i := 0; i < paramCountInt; i++ { - if len(buf) < minFieldLen { - return []Field{}, fmt.Errorf("invalid field length %v", len(buf)) - } - fieldID := buf[0:2] - fieldSize := buf[2:4] - fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4])) - expectedLen := minFieldLen + fieldSizeInt - if len(buf) < expectedLen { - return []Field{}, fmt.Errorf("field length too short") - } - - fields = append(fields, Field{ - ID: [2]byte(fieldID), - FieldSize: [2]byte(fieldSize), - Data: buf[4 : 4+fieldSizeInt], - }) - - buf = buf[fieldSizeInt+4:] - } - - if len(buf) != 0 { - return []Field{}, fmt.Errorf("extra field bytes") - } - - return fields, nil -} - // Read implements the io.Reader interface for Transaction func (t *Transaction) Read(p []byte) (int, error) { payloadSize := t.Size() @@ -253,16 +275,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[:])) { - return field + if id == field.Type { + return &field } } - return Field{} -} - -func (t *Transaction) IsError() bool { - return t.ErrorCode == [4]byte{0, 0, 0, 1} + return &Field{} }