-
Notifications
You must be signed in to change notification settings - Fork 1
/
client.go
338 lines (290 loc) · 8.06 KB
/
client.go
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
package battleye
import (
"net"
"sync"
"sync/atomic"
"time"
)
const (
// defaultTimeout is the default read / write timeout for the Client.
defaultTimeout = 10 * time.Second
// defaultKeepAlive is the default interval of sending keep-alive packets to the BattlEye server.
// It must be less than 45 seconds because BattlEye server consider a client disconnected
// if no command packets are received from it for more than 45 seconds.
defaultKeepAlive = 30 * time.Second
// defaultMessageBufferSize is the default buffer size of the msgs channel.
defaultMessageBufferSize = 100
// bufferSize is the size of the read buffer based on MTU.
bufferSize = 1500
// clientTimeout is the maximum duration after which the Client will be disconnected.
clientTimeout = 45 * time.Second
)
var (
// keepAliveCheck is the check interval for keepalives
keepAliveCheck = time.Second
)
// Client represents a BattlEye client.
type Client struct {
conn net.Conn
ctr uint64
timeout time.Duration
keepAlive time.Duration
msgBufSize int
wg sync.WaitGroup
fragments map[byte]*fragmentedResponse
sendLock sync.Mutex
lastLock sync.Mutex
lastSend time.Time
// done signals goroutines to stop.
done *done
// login is used for receiving the login response from the BattlEye server.
login chan bool
// cmds is used for receiving command-type responses from the BattlEye server.
cmds chan string
// msgs is a buffered channel which is used for getting broadcast messages from the BattlEye server.
msgs chan string
// errs is a channel for transmitting errors internally.
errs chan error
}
// NewClient returns a new BattlEye client connected to address.
func NewClient(addr string, pwd string, options ...Option) (*Client, error) {
c := &Client{
timeout: defaultTimeout,
keepAlive: defaultKeepAlive,
msgBufSize: defaultMessageBufferSize,
}
// Override defaults
for _, opt := range options {
if opt == nil {
return nil, ErrNilOption
}
if err := opt(c); err != nil {
return nil, err
}
}
c.done = newDone()
c.login = make(chan bool)
c.cmds = make(chan string)
c.msgs = make(chan string, c.msgBufSize)
c.errs = make(chan error)
c.fragments = make(map[byte]*fragmentedResponse)
if err := c.connect(addr, pwd); err != nil {
c.Close() // nolint: errcheck
return nil, err
}
return c, nil
}
// Close gracefully closes the connection.
func (c *Client) Close() error {
if c.done.IsDone() {
return nil
}
c.done.Done()
c.wg.Wait()
close(c.msgs)
return c.conn.Close()
}
// Messages returns a buffered channel containing the console messages sent by the server.
// If the channel is full new messages will be dropped. It is the user's responsibility
// to drain the channel and handle these messages.
func (c *Client) Messages() <-chan string {
return c.msgs
}
// Exec executes the cmd on the BattlEye server and returns its response.
// Executing is retried for 45 seconds after which the Client is considered to be disconnected
// and ErrTimeout is returned.
// A disconnected Client is unlikely to get any more responses from the BattlEye server, so
// a new Client should be created.
func (c *Client) Exec(cmd string) (string, error) {
c.sendLock.Lock()
defer c.sendLock.Unlock()
until := time.Now().Add(clientTimeout)
for time.Now().Before(until) {
resp, err := c.send(cmd)
if err != nil {
if err == ErrTimeout {
continue
}
return "", err
}
return resp, nil
}
// TODO: Is this fatal? Do we close the Client at this point?
return "", ErrTimeout
}
func (c *Client) send(cmd string) (string, error) {
if err := c.write(newCommandPacket(cmd, c.seq())); err != nil {
return "", err
}
c.lastLock.Lock()
c.lastSend = time.Now()
c.lastLock.Unlock()
t := time.NewTimer(c.timeout)
defer t.Stop()
select {
case <-t.C:
return "", ErrTimeout
case err := <-c.errs:
return "", err
case resp := <-c.cmds:
return resp, nil
}
}
// connect connects and authenticates Client to the BattlEye server.
func (c *Client) connect(addr, pwd string) (err error) {
c.conn, err = net.Dial("udp", addr)
if err != nil {
return err
}
c.wg.Add(1)
go c.receiver()
// Authenticate client.
if err := c.write(newLoginPacket(pwd)); err != nil {
return err
}
t := time.NewTimer(c.timeout)
select {
case <-t.C:
return ErrTimeout
case err := <-c.errs:
return err
case success := <-c.login:
if !success {
return ErrLoginFailed
}
}
// Client successfully logged in, start the keep-alive goroutine.
c.wg.Add(1)
go c.keepConnectionAlive()
return nil
}
// keepConnectionAlive is a goroutine which periodically sends a keep-alive packet to the BattlEye server.
func (c *Client) keepConnectionAlive() {
defer c.wg.Done()
t := time.NewTicker(keepAliveCheck)
for {
select {
case <-c.done.C():
t.Stop()
return
case <-t.C:
// To avoid potential deadlocks, this check should be done separately.
c.lastLock.Lock()
do := time.Since(c.lastSend) > c.keepAlive
c.lastLock.Unlock()
if do {
// Send an empty command, we don't care the response nor the error.
c.Exec("") // nolint: errcheck
}
}
}
}
// write writes a packet to conn.
func (c *Client) write(pkt *packet) error {
raw, err := pkt.bytes()
if err != nil {
return err
}
if err = c.setDeadline(); err != nil {
return err
}
_, err = c.conn.Write(raw)
return err
}
// receiver is a goroutine which reads responses from the connection and handles them according to
// their types.
func (c *Client) receiver() {
defer c.wg.Done()
for {
select {
case <-c.done.C():
return
default:
r, err := c.read()
if err != nil {
// Do not error in case of timeout.
if err, ok := err.(net.Error); ok && err.Timeout() {
continue
}
c.errs <- err
continue
}
switch r := r.(type) {
case bool:
c.login <- r
case *commandResponse:
c.handleCommandResponse(r)
case *serverMessage:
c.handleServerMessage(r)
}
}
}
}
// handleCommandResponse forwards CommandResponses to the cmds channel. If the message is
// fragmented it is reassembled beforehand.
func (c *Client) handleCommandResponse(r *commandResponse) {
// If the received response is either:
// - an old one that we've already processed (sequence number is less than what we expect);
// - or an unsolicited one (sequence number it totally different from what we expect);
// just drop it.
if r.seq != c.seq() {
return
}
// response is not fragmented.
if !r.multi {
c.incr()
c.cmds <- r.msg
return
}
// Add the partial message to the already received parts.
var fr *fragmentedResponse
fr, ok := c.fragments[r.seq]
if !ok {
fr = newFragmentedResponse(r.multiSize)
c.fragments[r.seq] = fr
}
fr.add(r)
// If the message is complete send it.
if fr.completed() {
c.incr()
c.cmds <- fr.message()
}
}
// handleServerMessage forwards the message part of ServerMessages to the msgs channel and
// sends back an acknowledge packet to the server.
func (c *Client) handleServerMessage(r *serverMessage) {
// If the channel is full, new messages will be dropped.
select {
case c.msgs <- r.msg:
default:
}
// Client has to acknowledge the server message by sending back its sequence number.
// No response is expected from the server.
// We don't care write errors.
c.write(newServerMessageAcknowledgePacket(r.seq)) // nolint: errcheck
}
// read reads from conn and parses the raw data as response.
func (c *Client) read() (interface{}, error) {
if err := c.setDeadline(); err != nil {
return nil, err
}
// As there is no size in the battleye protocol we must assume each read returns a single response.
b := make([]byte, bufferSize)
n, err := c.conn.Read(b)
if err != nil {
return nil, err
}
return parseResponse(b[:n])
}
// seq returns the command sequence number counter.
func (c *Client) seq() byte {
return byte(atomic.LoadUint64(&c.ctr))
}
// incr increments the command sequence number counter.
func (c *Client) incr() {
atomic.AddUint64(&c.ctr, 1)
}
// setDeadline updates the deadline on the connection based on the clients configured timeout.
func (c *Client) setDeadline() error {
return c.conn.SetDeadline(time.Now().Add(c.timeout))
}