]> git.r.bdr.sh - rbdr/mobius/blob - hotline/transaction.go
Merge pull request #55 from jhalter/add_client_error_messaging
[rbdr/mobius] / hotline / transaction.go
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 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 // ReadTransaction parses a byte slice into a struct. The input slice may be shorter or longer
106 // that the transaction size depending on what was read from the network connection.
107 func ReadTransaction(buf []byte) (*Transaction, int, error) {
108 totalSize := binary.BigEndian.Uint32(buf[12:16])
109
110 // the buf may include extra bytes that are not part of the transaction
111 // tranLen represents the length of bytes that are part of the transaction
112 tranLen := int(20 + totalSize)
113
114 if tranLen > len(buf) {
115 return nil, 0, errors.New("buflen too small for tranLen")
116 }
117 fields, err := ReadFields(buf[20:22], buf[22:tranLen])
118 if err != nil {
119 return nil, 0, err
120 }
121
122 return &Transaction{
123 Flags: buf[0],
124 IsReply: buf[1],
125 Type: buf[2:4],
126 ID: buf[4:8],
127 ErrorCode: buf[8:12],
128 TotalSize: buf[12:16],
129 DataSize: buf[16:20],
130 ParamCount: buf[20:22],
131 Fields: fields,
132 }, tranLen, nil
133 }
134
135 const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields
136
137 // transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
138 func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) {
139 // The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes
140 if len(data) < 16 {
141 return 0, nil, nil
142 }
143
144 totalSize := binary.BigEndian.Uint32(data[12:16])
145
146 // tranLen represents the length of bytes that are part of the transaction
147 tranLen := int(tranHeaderLen + totalSize)
148 if tranLen > len(data) {
149 return 0, nil, nil
150 }
151
152 return tranLen, data[0:tranLen], nil
153 }
154
155 const minFieldLen = 4
156
157 func ReadFields(paramCount []byte, buf []byte) ([]Field, error) {
158 paramCountInt := int(binary.BigEndian.Uint16(paramCount))
159 if paramCountInt > 0 && len(buf) < minFieldLen {
160 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
161 }
162
163 // A Field consists of:
164 // ID: 2 bytes
165 // Size: 2 bytes
166 // Data: FieldSize number of bytes
167 var fields []Field
168 for i := 0; i < paramCountInt; i++ {
169 if len(buf) < minFieldLen {
170 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
171 }
172 fieldID := buf[0:2]
173 fieldSize := buf[2:4]
174 fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4]))
175 expectedLen := minFieldLen + fieldSizeInt
176 if len(buf) < expectedLen {
177 return []Field{}, fmt.Errorf("field length too short")
178 }
179
180 fields = append(fields, Field{
181 ID: fieldID,
182 FieldSize: fieldSize,
183 Data: buf[4 : 4+fieldSizeInt],
184 })
185
186 buf = buf[fieldSizeInt+4:]
187 }
188
189 if len(buf) != 0 {
190 return []Field{}, fmt.Errorf("extra field bytes")
191 }
192
193 return fields, nil
194 }
195
196 func (t *Transaction) MarshalBinary() (data []byte, err error) {
197 payloadSize := t.Size()
198
199 fieldCount := make([]byte, 2)
200 binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
201
202 var fieldPayload []byte
203 for _, field := range t.Fields {
204 fieldPayload = append(fieldPayload, field.Payload()...)
205 }
206
207 return concat.Slices(
208 []byte{t.Flags, t.IsReply},
209 t.Type,
210 t.ID,
211 t.ErrorCode,
212 payloadSize,
213 payloadSize, // this is the dataSize field, but seeming the same as totalSize
214 fieldCount,
215 fieldPayload,
216 ), err
217 }
218
219 // Size returns the total size of the transaction payload
220 func (t *Transaction) Size() []byte {
221 bs := make([]byte, 4)
222
223 fieldSize := 0
224 for _, field := range t.Fields {
225 fieldSize += len(field.Data) + 4
226 }
227
228 binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
229
230 return bs
231 }
232
233 func (t *Transaction) GetField(id int) Field {
234 for _, field := range t.Fields {
235 if id == int(binary.BigEndian.Uint16(field.ID)) {
236 return field
237 }
238 }
239
240 return Field{}
241 }
242
243 func (t *Transaction) IsError() bool {
244 return bytes.Compare(t.ErrorCode, []byte{0, 0, 0, 1}) == 0
245 }