]> git.r.bdr.sh - rbdr/mobius/blob - hotline/transaction.go
Implement Make Alias transaction
[rbdr/mobius] / hotline / transaction.go
1 package hotline
2
3 import (
4 "encoding/binary"
5 "errors"
6 "fmt"
7 "github.com/jhalter/mobius/concat"
8 "math/rand"
9 "net"
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 tranGetFileNameList = 200
36 tranDownloadFile = 202
37 tranUploadFile = 203
38 tranNewFolder = 205
39 tranDeleteFile = 204
40 tranGetFileInfo = 206
41 tranSetFileInfo = 207
42 tranMoveFile = 208
43 tranMakeFileAlias = 209 // TODO: implement file alias command
44 tranDownloadFldr = 210
45 // tranDownloadInfo = 211 TODO: implement file transfer queue
46 // tranDownloadBanner = 212 TODO: figure out what this is used for
47 tranUploadFldr = 213
48 tranGetUserNameList = 300
49 tranNotifyChangeUser = 301
50 tranNotifyDeleteUser = 302
51 tranGetClientInfoText = 303
52 tranSetClientUserInfo = 304
53 tranListUsers = 348
54 // tranUpdateUser = 349 TODO: implement user updates from the > 1.5 account editor
55 tranNewUser = 350
56 tranDeleteUser = 351
57 tranGetUser = 352
58 tranSetUser = 353
59 tranUserAccess = 354
60 tranUserBroadcast = 355
61 tranGetNewsCatNameList = 370
62 tranGetNewsArtNameList = 371
63 tranDelNewsItem = 380
64 tranNewNewsFldr = 381
65 tranNewNewsCat = 382
66 tranGetNewsArtData = 400
67 tranPostNewsArt = 410
68 tranDelNewsArt = 411
69 tranKeepAlive = 500
70 )
71
72 type Transaction struct {
73 clientID *[]byte
74
75 Flags byte // Reserved (should be 0)
76 IsReply byte // Request (0) or reply (1)
77 Type []byte // Requested operation (user defined)
78 ID []byte // Unique transaction ID (must be != 0)
79 ErrorCode []byte // Used in the reply (user defined, 0 = no error)
80 TotalSize []byte // Total data size for the transaction (all parts)
81 DataSize []byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
82 ParamCount []byte // Number of the parameters for this transaction
83 Fields []Field
84 }
85
86 func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction {
87 typeSlice := make([]byte, 2)
88 binary.BigEndian.PutUint16(typeSlice, uint16(t))
89
90 idSlice := make([]byte, 4)
91 binary.BigEndian.PutUint32(idSlice, rand.Uint32())
92
93 return &Transaction{
94 clientID: clientID,
95 Flags: 0x00,
96 IsReply: 0x00,
97 Type: typeSlice,
98 ID: idSlice,
99 ErrorCode: []byte{0, 0, 0, 0},
100 Fields: fields,
101 }
102 }
103
104 // ReadTransaction parses a byte slice into a struct. The input slice may be shorter or longer
105 // that the transaction size depending on what was read from the network connection.
106 func ReadTransaction(buf []byte) (*Transaction, int, error) {
107 totalSize := binary.BigEndian.Uint32(buf[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(buf) {
114 return nil, 0, errors.New("buflen too small for tranLen")
115 }
116 fields, err := ReadFields(buf[20:22], buf[22:tranLen])
117 if err != nil {
118 return nil, 0, err
119 }
120
121 return &Transaction{
122 Flags: buf[0],
123 IsReply: buf[1],
124 Type: buf[2:4],
125 ID: buf[4:8],
126 ErrorCode: buf[8:12],
127 TotalSize: buf[12:16],
128 DataSize: buf[16:20],
129 ParamCount: buf[20:22],
130 Fields: fields,
131 }, tranLen, nil
132 }
133
134 func readN(conn net.Conn, n int) ([]Transaction, error) {
135 buf := make([]byte, 1400)
136 i := 0
137 for {
138 readLen, err := conn.Read(buf)
139 if err != nil {
140 return nil, err
141 }
142
143 transactions, _, err := readTransactions(buf[:readLen])
144 // spew.Fdump(os.Stderr, transactions)
145 if err != nil {
146 return nil, err
147 }
148
149 i += len(transactions)
150
151 if n == i {
152 return transactions, nil
153 }
154 }
155 }
156
157 func readTransactions(buf []byte) ([]Transaction, int, error) {
158 var transactions []Transaction
159
160 bufLen := len(buf)
161
162 var bytesRead = 0
163 for bytesRead < bufLen {
164 t, tReadLen, err := ReadTransaction(buf[bytesRead:])
165 if err != nil {
166 return transactions, bytesRead, err
167 }
168 bytesRead += tReadLen
169
170 transactions = append(transactions, *t)
171 }
172
173 return transactions, bytesRead, nil
174 }
175
176 const minFieldLen = 4
177
178 func ReadFields(paramCount []byte, buf []byte) ([]Field, error) {
179 paramCountInt := int(binary.BigEndian.Uint16(paramCount))
180 if paramCountInt > 0 && len(buf) < minFieldLen {
181 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
182 }
183
184 // A Field consists of:
185 // ID: 2 bytes
186 // Size: 2 bytes
187 // Data: FieldSize number of bytes
188 var fields []Field
189 for i := 0; i < paramCountInt; i++ {
190 if len(buf) < minFieldLen {
191 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
192 }
193 fieldID := buf[0:2]
194 fieldSize := buf[2:4]
195 fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4]))
196 expectedLen := minFieldLen + fieldSizeInt
197 if len(buf) < expectedLen {
198 return []Field{}, fmt.Errorf("field length too short")
199 }
200
201 fields = append(fields, Field{
202 ID: fieldID,
203 FieldSize: fieldSize,
204 Data: buf[4 : 4+fieldSizeInt],
205 })
206
207 buf = buf[fieldSizeInt+4:]
208 }
209
210 if len(buf) != 0 {
211 return []Field{}, fmt.Errorf("extra field bytes")
212 }
213
214 return fields, nil
215 }
216
217 func (t Transaction) MarshalBinary() (data []byte, err error) {
218 payloadSize := t.Size()
219
220 fieldCount := make([]byte, 2)
221 binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
222
223 var fieldPayload []byte
224 for _, field := range t.Fields {
225 fieldPayload = append(fieldPayload, field.Payload()...)
226 }
227
228 return concat.Slices(
229 []byte{t.Flags, t.IsReply},
230 t.Type,
231 t.ID,
232 t.ErrorCode,
233 payloadSize,
234 payloadSize, // this is the dataSize field, but seeming the same as totalSize
235 fieldCount,
236 fieldPayload,
237 ), err
238 }
239
240 // Size returns the total size of the transaction payload
241 func (t Transaction) Size() []byte {
242 bs := make([]byte, 4)
243
244 fieldSize := 0
245 for _, field := range t.Fields {
246 fieldSize += len(field.Data) + 4
247 }
248
249 binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
250
251 return bs
252 }
253
254 func (t Transaction) GetField(id int) Field {
255 for _, field := range t.Fields {
256 if id == int(binary.BigEndian.Uint16(field.ID)) {
257 return field
258 }
259 }
260
261 return Field{}
262 }