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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
package hotline
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math/rand"
"slices"
)
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 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 ClientID // Internal identifier for target client
readOffset int // Internal offset to track read progress
}
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",
}
// 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.
// 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) {
// Make sure we have the minimum number of bytes for a transaction.
if len(p) < 22 {
return 0, errors.New("buffer too small")
}
// Read the total size field.
totalSize := binary.BigEndian.Uint32(p[12:16])
tranLen := int(20 + totalSize)
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])
scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen]))
scanner.Split(FieldScanner)
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 {
return 0, fmt.Errorf("error reading field: %w", err)
}
t.Fields = append(t.Fields, field)
}
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
// 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
// Read implements the io.Reader interface for Transaction
func (t *Transaction) Read(p []byte) (int, error) {
payloadSize := t.Size()
fieldCount := make([]byte, 2)
binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
bbuf := new(bytes.Buffer)
for _, field := range t.Fields {
f := field
_, err := bbuf.ReadFrom(&f)
if err != nil {
return 0, fmt.Errorf("error reading field: %w", err)
}
}
buf := slices.Concat(
[]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,
bbuf.Bytes(),
)
if t.readOffset >= len(buf) {
return 0, io.EOF // All bytes have been read
}
n := copy(p, buf[t.readOffset:])
t.readOffset += n
return n, nil
}
// 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 FieldType) *Field {
for _, field := range t.Fields {
if id == field.Type {
return &field
}
}
return &Field{}
}
|