github.com/jhalter/mobius@v0.12.1/hotline/transaction.go (about) 1 package hotline 2 3 import ( 4 "bytes" 5 "encoding/binary" 6 "errors" 7 "fmt" 8 "github.com/jhalter/mobius/concat" 9 "math/rand" 10 ) 11 12 const ( 13 TranError = 0 14 TranGetMsgs = 101 15 TranNewMsg = 102 16 TranOldPostNews = 103 17 TranServerMsg = 104 18 TranChatSend = 105 19 TranChatMsg = 106 20 TranLogin = 107 21 TranSendInstantMsg = 108 22 TranShowAgreement = 109 23 TranDisconnectUser = 110 24 TranDisconnectMsg = 111 // TODO: implement server initiated friendly disconnect 25 TranInviteNewChat = 112 26 TranInviteToChat = 113 27 TranRejectChatInvite = 114 28 TranJoinChat = 115 29 TranLeaveChat = 116 30 TranNotifyChatChangeUser = 117 31 TranNotifyChatDeleteUser = 118 32 TranNotifyChatSubject = 119 33 TranSetChatSubject = 120 34 TranAgreed = 121 35 TranServerBanner = 122 36 TranGetFileNameList = 200 37 TranDownloadFile = 202 38 TranUploadFile = 203 39 TranNewFolder = 205 40 TranDeleteFile = 204 41 TranGetFileInfo = 206 42 TranSetFileInfo = 207 43 TranMoveFile = 208 44 TranMakeFileAlias = 209 45 TranDownloadFldr = 210 46 TranDownloadInfo = 211 // TODO: implement file transfer queue 47 TranDownloadBanner = 212 48 TranUploadFldr = 213 49 TranGetUserNameList = 300 50 TranNotifyChangeUser = 301 51 TranNotifyDeleteUser = 302 52 TranGetClientInfoText = 303 53 TranSetClientUserInfo = 304 54 TranListUsers = 348 55 TranUpdateUser = 349 56 TranNewUser = 350 57 TranDeleteUser = 351 58 TranGetUser = 352 59 TranSetUser = 353 60 TranUserAccess = 354 61 TranUserBroadcast = 355 62 TranGetNewsCatNameList = 370 63 TranGetNewsArtNameList = 371 64 TranDelNewsItem = 380 65 TranNewNewsFldr = 381 66 TranNewNewsCat = 382 67 TranGetNewsArtData = 400 68 TranPostNewsArt = 410 69 TranDelNewsArt = 411 70 TranKeepAlive = 500 71 ) 72 73 type Transaction struct { 74 clientID *[]byte 75 76 Flags byte // Reserved (should be 0) 77 IsReply byte // Request (0) or reply (1) 78 Type []byte // Requested operation (user defined) 79 ID []byte // Unique transaction ID (must be != 0) 80 ErrorCode []byte // Used in the reply (user defined, 0 = no error) 81 TotalSize []byte // Total data size for the transaction (all parts) 82 DataSize []byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts. 83 ParamCount []byte // Number of the parameters for this transaction 84 Fields []Field 85 } 86 87 func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction { 88 typeSlice := make([]byte, 2) 89 binary.BigEndian.PutUint16(typeSlice, uint16(t)) 90 91 idSlice := make([]byte, 4) 92 binary.BigEndian.PutUint32(idSlice, rand.Uint32()) 93 94 return &Transaction{ 95 clientID: clientID, 96 Flags: 0x00, 97 IsReply: 0x00, 98 Type: typeSlice, 99 ID: idSlice, 100 ErrorCode: []byte{0, 0, 0, 0}, 101 Fields: fields, 102 } 103 } 104 105 // Write implements io.Writer interface for Transaction 106 func (t *Transaction) Write(p []byte) (n int, err error) { 107 totalSize := binary.BigEndian.Uint32(p[12:16]) 108 109 // the buf may include extra bytes that are not part of the transaction 110 // tranLen represents the length of bytes that are part of the transaction 111 tranLen := int(20 + totalSize) 112 113 if tranLen > len(p) { 114 return n, errors.New("buflen too small for tranLen") 115 } 116 fields, err := ReadFields(p[20:22], p[22:tranLen]) 117 if err != nil { 118 return n, err 119 } 120 121 t.Flags = p[0] 122 t.IsReply = p[1] 123 t.Type = p[2:4] 124 t.ID = p[4:8] 125 t.ErrorCode = p[8:12] 126 t.TotalSize = p[12:16] 127 t.DataSize = p[16:20] 128 t.ParamCount = p[20:22] 129 t.Fields = fields 130 131 return len(p), err 132 } 133 134 const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields 135 136 // transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens 137 func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) { 138 // The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes 139 if len(data) < 16 { 140 return 0, nil, nil 141 } 142 143 totalSize := binary.BigEndian.Uint32(data[12:16]) 144 145 // tranLen represents the length of bytes that are part of the transaction 146 tranLen := int(tranHeaderLen + totalSize) 147 if tranLen > len(data) { 148 return 0, nil, nil 149 } 150 151 return tranLen, data[0:tranLen], nil 152 } 153 154 const minFieldLen = 4 155 156 func ReadFields(paramCount []byte, buf []byte) ([]Field, error) { 157 paramCountInt := int(binary.BigEndian.Uint16(paramCount)) 158 if paramCountInt > 0 && len(buf) < minFieldLen { 159 return []Field{}, fmt.Errorf("invalid field length %v", len(buf)) 160 } 161 162 // A Field consists of: 163 // ID: 2 bytes 164 // Size: 2 bytes 165 // Data: FieldSize number of bytes 166 var fields []Field 167 for i := 0; i < paramCountInt; i++ { 168 if len(buf) < minFieldLen { 169 return []Field{}, fmt.Errorf("invalid field length %v", len(buf)) 170 } 171 fieldID := buf[0:2] 172 fieldSize := buf[2:4] 173 fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4])) 174 expectedLen := minFieldLen + fieldSizeInt 175 if len(buf) < expectedLen { 176 return []Field{}, fmt.Errorf("field length too short") 177 } 178 179 fields = append(fields, Field{ 180 ID: fieldID, 181 FieldSize: fieldSize, 182 Data: buf[4 : 4+fieldSizeInt], 183 }) 184 185 buf = buf[fieldSizeInt+4:] 186 } 187 188 if len(buf) != 0 { 189 return []Field{}, fmt.Errorf("extra field bytes") 190 } 191 192 return fields, nil 193 } 194 195 func (t *Transaction) MarshalBinary() (data []byte, err error) { 196 payloadSize := t.Size() 197 198 fieldCount := make([]byte, 2) 199 binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields))) 200 201 var fieldPayload []byte 202 for _, field := range t.Fields { 203 fieldPayload = append(fieldPayload, field.Payload()...) 204 } 205 206 return concat.Slices( 207 []byte{t.Flags, t.IsReply}, 208 t.Type, 209 t.ID, 210 t.ErrorCode, 211 payloadSize, 212 payloadSize, // this is the dataSize field, but seeming the same as totalSize 213 fieldCount, 214 fieldPayload, 215 ), err 216 } 217 218 // Size returns the total size of the transaction payload 219 func (t *Transaction) Size() []byte { 220 bs := make([]byte, 4) 221 222 fieldSize := 0 223 for _, field := range t.Fields { 224 fieldSize += len(field.Data) + 4 225 } 226 227 binary.BigEndian.PutUint32(bs, uint32(fieldSize+2)) 228 229 return bs 230 } 231 232 func (t *Transaction) GetField(id int) Field { 233 for _, field := range t.Fields { 234 if id == int(binary.BigEndian.Uint16(field.ID)) { 235 return field 236 } 237 } 238 239 return Field{} 240 } 241 242 func (t *Transaction) IsError() bool { 243 return bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 1}) 244 }