1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
|
package hotline
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"github.com/jhalter/mobius/concat"
"math/rand"
)
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 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 Transaction struct {
clientID *[]byte
Flags byte // Reserved (should be 0)
IsReply byte // Request (0) or reply (1)
Type []byte // Requested operation (user defined)
ID []byte // Unique transaction ID (must be != 0)
ErrorCode []byte // Used in the reply (user defined, 0 = no error)
TotalSize []byte // Total data size for the transaction (all parts)
DataSize []byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
ParamCount []byte // Number of the parameters for this transaction
Fields []Field
}
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())
return &Transaction{
clientID: clientID,
Flags: 0x00,
IsReply: 0x00,
Type: typeSlice,
ID: idSlice,
ErrorCode: []byte{0, 0, 0, 0},
Fields: fields,
}
}
// ReadTransaction parses a byte slice into a struct. The input slice may be shorter or longer
// that the transaction size depending on what was read from the network connection.
func ReadTransaction(buf []byte) (*Transaction, int, error) {
totalSize := binary.BigEndian.Uint32(buf[12:16])
// 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
tranLen := int(20 + totalSize)
if tranLen > len(buf) {
return nil, 0, errors.New("buflen too small for tranLen")
}
fields, err := ReadFields(buf[20:22], buf[22:tranLen])
if err != nil {
return nil, 0, err
}
return &Transaction{
Flags: buf[0],
IsReply: buf[1],
Type: buf[2:4],
ID: buf[4:8],
ErrorCode: buf[8:12],
TotalSize: buf[12:16],
DataSize: buf[16:20],
ParamCount: buf[20:22],
Fields: fields,
}, tranLen, nil
}
const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields
// transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) {
// The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes
if len(data) < 16 {
return 0, nil, nil
}
totalSize := binary.BigEndian.Uint32(data[12:16])
// tranLen represents the length of bytes that are part of the transaction
tranLen := int(tranHeaderLen + totalSize)
if tranLen > len(data) {
return 0, nil, nil
}
return tranLen, data[0:tranLen], nil
}
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: fieldID,
FieldSize: fieldSize,
Data: buf[4 : 4+fieldSizeInt],
})
buf = buf[fieldSizeInt+4:]
}
if len(buf) != 0 {
return []Field{}, fmt.Errorf("extra field bytes")
}
return fields, nil
}
func (t *Transaction) MarshalBinary() (data []byte, err error) {
payloadSize := t.Size()
fieldCount := make([]byte, 2)
binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
var fieldPayload []byte
for _, field := range t.Fields {
fieldPayload = append(fieldPayload, field.Payload()...)
}
return concat.Slices(
[]byte{t.Flags, t.IsReply},
t.Type,
t.ID,
t.ErrorCode,
payloadSize,
payloadSize, // this is the dataSize field, but seeming the same as totalSize
fieldCount,
fieldPayload,
), err
}
// Size returns the total size of the transaction payload
func (t *Transaction) Size() []byte {
bs := make([]byte, 4)
fieldSize := 0
for _, field := range t.Fields {
fieldSize += len(field.Data) + 4
}
binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
return bs
}
func (t *Transaction) GetField(id int) Field {
for _, field := range t.Fields {
if id == int(binary.BigEndian.Uint16(field.ID)) {
return field
}
}
return Field{}
}
func (t *Transaction) IsError() bool {
return bytes.Compare(t.ErrorCode, []byte{0, 0, 0, 1}) == 0
}
|