forked from dchote/gumble
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
291 lines (248 loc) · 7.23 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
package gumble // import "github.com/5pm-HDH/gumble/gumble"
import (
"crypto/tls"
"errors"
"math"
"net"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/5pm-HDH/gumble/gumble/MumbleProto"
"github.com/golang/protobuf/proto"
)
// State is the current state of the client's connection to the server.
type State int
const (
// StateDisconnected means the client is no longer connected to the server.
StateDisconnected State = iota
// StateConnected means the client is connected to the server and is
// syncing initial information. This is an internal state that will
// never be returned by Client.State().
StateConnected
// StateSynced means the client is connected to a server and has been sent
// the server state.
StateSynced
)
// ClientVersion is the protocol version that Client implements.
const ClientVersion = 1<<16 | 3<<8 | 0
// Client is the type used to create a connection to a server.
type Client struct {
// The User associated with the client.
Self *User
// The client's configuration.
Config *Config
// The underlying Conn to the server.
Conn *Conn
// The users currently connected to the server.
Users Users
// The connected server's channels.
Channels Channels
permissions map[uint32]*Permission
tmpACL *ACL
// Ping stats
tcpPacketsReceived uint32
tcpPingTimes [12]float32
tcpPingAvg uint32
tcpPingVar uint32
// A collection containing the server's context actions.
ContextActions ContextActions
// The audio encoder used when sending audio to the server.
AudioEncoder AudioEncoder
audioCodec AudioCodec
// To whom transmitted audio will be sent. The VoiceTarget must have already
// been sent to the server for targeting to work correctly. Setting to nil
// will disable voice targeting (i.e. switch back to regular speaking).
VoiceTarget *VoiceTarget
state uint32
volatileWg sync.WaitGroup
volatileLock sync.Mutex
connect chan *RejectError
end chan struct{}
disconnectEvent DisconnectEvent
}
// Dial is an alias of DialWithDialer(new(net.Dialer), addr, config, nil).
func Dial(addr string, config *Config) (*Client, error) {
return DialWithDialer(new(net.Dialer), addr, config, nil)
}
// DialWithDialer connects to the Mumble server at the given address.
//
// The function returns after the connection has been established, the initial
// server information has been synced, and the OnConnect handlers have been
// called.
//
// nil and an error is returned if server synchronization does not complete by
// min(time.Now() + dialer.Timeout, dialer.Deadline), or if the server rejects
// the client.
func DialWithDialer(dialer *net.Dialer, addr string, config *Config, tlsConfig *tls.Config) (*Client, error) {
start := time.Now()
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
if err != nil {
return nil, err
}
client := &Client{
Conn: NewConn(conn),
Config: config,
Users: make(Users),
Channels: make(Channels),
permissions: make(map[uint32]*Permission),
state: uint32(StateConnected),
connect: make(chan *RejectError),
end: make(chan struct{}),
}
go client.readRoutine()
// Initial packets
versionPacket := MumbleProto.Version{
Version: proto.Uint32(ClientVersion),
Release: proto.String("gumble"),
Os: proto.String(runtime.GOOS),
OsVersion: proto.String(runtime.GOARCH),
}
authenticationPacket := MumbleProto.Authenticate{
Username: &client.Config.Username,
Password: &client.Config.Password,
Opus: proto.Bool(getAudioCodec(audioCodecIDOpus) != nil),
Tokens: client.Config.Tokens,
}
client.Conn.WriteProto(&versionPacket)
client.Conn.WriteProto(&authenticationPacket)
go client.pingRoutine()
var deadline time.Time
if !dialer.Deadline.IsZero() {
deadline = dialer.Deadline
}
if dialer.Timeout > 0 {
diff := start.Add(dialer.Timeout)
if deadline.IsZero() || diff.Before(deadline) {
deadline = diff
}
}
if !deadline.IsZero() {
timeout := deadline.Sub(start)
select {
case <-time.After(timeout):
client.Conn.Close()
return nil, errors.New("gumble: synchronization timeout")
case err := <-client.connect:
if err != nil {
client.Conn.Close()
return nil, err
}
}
} else {
if err := <-client.connect; err != nil {
client.Conn.Close()
return nil, err
}
}
return client, nil
}
// State returns the current state of the client.
func (c *Client) State() State {
return State(atomic.LoadUint32(&c.state))
}
// AudioOutgoing creates a new channel that outgoing audio data can be written
// to. The channel must be closed after the audio stream is completed. Only
// a single channel should be open at any given time (i.e. close the channel
// before opening another).
func (c *Client) AudioOutgoing() chan<- AudioBuffer {
ch := make(chan AudioBuffer)
go func() {
var seq int64
previous := <-ch
for p := range ch {
previous.writeAudio(c, seq, false)
previous = p
seq = (seq + 1) % math.MaxInt32
}
if previous != nil {
previous.writeAudio(c, seq, true)
}
}()
return ch
}
// pingRoutine sends ping packets to the server at regular intervals.
func (c *Client) pingRoutine() {
ticker := time.NewTicker(time.Second * 5)
defer ticker.Stop()
var timestamp uint64
var tcpPingAvg float32
var tcpPingVar float32
packet := MumbleProto.Ping{
Timestamp: ×tamp,
TcpPackets: &c.tcpPacketsReceived,
TcpPingAvg: &tcpPingAvg,
TcpPingVar: &tcpPingVar,
}
t := time.Now()
for {
timestamp = uint64(t.UnixNano())
tcpPingAvg = math.Float32frombits(atomic.LoadUint32(&c.tcpPingAvg))
tcpPingVar = math.Float32frombits(atomic.LoadUint32(&c.tcpPingVar))
c.Conn.WriteProto(&packet)
select {
case <-c.end:
return
case t = <-ticker.C:
// continue to top of loop
}
}
}
// readRoutine reads protocol buffer messages from the server.
func (c *Client) readRoutine() {
c.disconnectEvent = DisconnectEvent{
Client: c,
Type: DisconnectError,
}
for {
pType, data, err := c.Conn.ReadPacket()
if err != nil {
break
}
if int(pType) < len(handlers) {
handlers[pType](c, data)
}
}
wasSynced := c.State() == StateSynced
atomic.StoreUint32(&c.state, uint32(StateDisconnected))
close(c.end)
if wasSynced {
c.Config.Listeners.onDisconnect(&c.disconnectEvent)
}
}
// RequestUserList requests that the server's registered user list be sent to
// the client.
func (c *Client) RequestUserList() {
packet := MumbleProto.UserList{}
c.Conn.WriteProto(&packet)
}
// RequestBanList requests that the server's ban list be sent to the client.
func (c *Client) RequestBanList() {
packet := MumbleProto.BanList{
Query: proto.Bool(true),
}
c.Conn.WriteProto(&packet)
}
// Disconnect disconnects the client from the server.
func (c *Client) Disconnect() error {
if c.State() == StateDisconnected {
return errors.New("gumble: client is already disconnected")
}
c.disconnectEvent.Type = DisconnectUser
c.Conn.Close()
return nil
}
// Do executes f in a thread-safe manner. It ensures that Client and its
// associated data will not be changed during the lifetime of the function
// call.
func (c *Client) Do(f func()) {
c.volatileLock.Lock()
c.volatileWg.Add(1)
c.volatileLock.Unlock()
defer c.volatileWg.Done()
f()
}
// Send will send a Message to the server.
func (c *Client) Send(message Message) {
message.writeMessage(c)
}