1package gumble
2
3import (
4	"crypto/tls"
5	"errors"
6	"math"
7	"net"
8	"runtime"
9	"sync/atomic"
10	"time"
11
12	"github.com/golang/protobuf/proto"
13	"layeh.com/gumble/gumble/MumbleProto"
14)
15
16// State is the current state of the client's connection to the server.
17type State int
18
19const (
20	// StateDisconnected means the client is no longer connected to the server.
21	StateDisconnected State = iota
22
23	// StateConnected means the client is connected to the server and is
24	// syncing initial information. This is an internal state that will
25	// never be returned by Client.State().
26	StateConnected
27
28	// StateSynced means the client is connected to a server and has been sent
29	// the server state.
30	StateSynced
31)
32
33// ClientVersion is the protocol version that Client implements.
34const ClientVersion = 1<<16 | 3<<8 | 0
35
36// Client is the type used to create a connection to a server.
37type Client struct {
38	// The User associated with the client.
39	Self *User
40	// The client's configuration.
41	Config *Config
42	// The underlying Conn to the server.
43	Conn *Conn
44
45	// The users currently connected to the server.
46	Users Users
47	// The connected server's channels.
48	Channels    Channels
49	permissions map[uint32]*Permission
50	tmpACL      *ACL
51
52	// Ping stats
53	tcpPacketsReceived uint32
54	tcpPingTimes       [12]float32
55	tcpPingAvg         uint32
56	tcpPingVar         uint32
57
58	// A collection containing the server's context actions.
59	ContextActions ContextActions
60
61	// The audio encoder used when sending audio to the server.
62	AudioEncoder AudioEncoder
63	audioCodec   AudioCodec
64	// To whom transmitted audio will be sent. The VoiceTarget must have already
65	// been sent to the server for targeting to work correctly. Setting to nil
66	// will disable voice targeting (i.e. switch back to regular speaking).
67	VoiceTarget *VoiceTarget
68
69	state uint32
70
71	// volatile is held by the client when the internal data structures are being
72	// modified.
73	volatile rpwMutex
74
75	connect         chan *RejectError
76	end             chan struct{}
77	disconnectEvent DisconnectEvent
78}
79
80// Dial is an alias of DialWithDialer(new(net.Dialer), addr, config, nil).
81func Dial(addr string, config *Config) (*Client, error) {
82	return DialWithDialer(new(net.Dialer), addr, config, nil)
83}
84
85// DialWithDialer connects to the Mumble server at the given address.
86//
87// The function returns after the connection has been established, the initial
88// server information has been synced, and the OnConnect handlers have been
89// called.
90//
91// nil and an error is returned if server synchronization does not complete by
92// min(time.Now() + dialer.Timeout, dialer.Deadline), or if the server rejects
93// the client.
94func DialWithDialer(dialer *net.Dialer, addr string, config *Config, tlsConfig *tls.Config) (*Client, error) {
95	start := time.Now()
96
97	conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
98	if err != nil {
99		return nil, err
100	}
101
102	client := &Client{
103		Conn:     NewConn(conn),
104		Config:   config,
105		Users:    make(Users),
106		Channels: make(Channels),
107
108		permissions: make(map[uint32]*Permission),
109
110		state: uint32(StateConnected),
111
112		connect: make(chan *RejectError),
113		end:     make(chan struct{}),
114	}
115
116	go client.readRoutine()
117
118	// Initial packets
119	versionPacket := MumbleProto.Version{
120		Version:   proto.Uint32(ClientVersion),
121		Release:   proto.String("gumble"),
122		Os:        proto.String(runtime.GOOS),
123		OsVersion: proto.String(runtime.GOARCH),
124	}
125	authenticationPacket := MumbleProto.Authenticate{
126		Username: &client.Config.Username,
127		Password: &client.Config.Password,
128		Opus:     proto.Bool(getAudioCodec(audioCodecIDOpus) != nil),
129		Tokens:   client.Config.Tokens,
130	}
131	client.Conn.WriteProto(&versionPacket)
132	client.Conn.WriteProto(&authenticationPacket)
133
134	go client.pingRoutine()
135
136	var timeout <-chan time.Time
137	{
138		var deadline time.Time
139		if !dialer.Deadline.IsZero() {
140			deadline = dialer.Deadline
141		}
142		if dialer.Timeout > 0 {
143			diff := start.Add(dialer.Timeout)
144			if deadline.IsZero() || diff.Before(deadline) {
145				deadline = diff
146			}
147		}
148		if !deadline.IsZero() {
149			timer := time.NewTimer(deadline.Sub(start))
150			defer timer.Stop()
151			timeout = timer.C
152		}
153	}
154
155	select {
156	case <-timeout:
157		client.Conn.Close()
158		return nil, errors.New("gumble: synchronization timeout")
159	case err := <-client.connect:
160		if err != nil {
161			client.Conn.Close()
162			return nil, err
163		}
164
165		return client, nil
166	}
167}
168
169// State returns the current state of the client.
170func (c *Client) State() State {
171	return State(atomic.LoadUint32(&c.state))
172}
173
174// AudioOutgoing creates a new channel that outgoing audio data can be written
175// to. The channel must be closed after the audio stream is completed. Only
176// a single channel should be open at any given time (i.e. close the channel
177// before opening another).
178func (c *Client) AudioOutgoing() chan<- AudioBuffer {
179	ch := make(chan AudioBuffer)
180	go func() {
181		var seq int64
182		previous := <-ch
183		for p := range ch {
184			previous.writeAudio(c, seq, false)
185			previous = p
186			seq = (seq + 1) % math.MaxInt32
187		}
188		if previous != nil {
189			previous.writeAudio(c, seq, true)
190		}
191	}()
192	return ch
193}
194
195// pingRoutine sends ping packets to the server at regular intervals.
196func (c *Client) pingRoutine() {
197	ticker := time.NewTicker(time.Second * 5)
198	defer ticker.Stop()
199
200	var timestamp uint64
201	var tcpPingAvg float32
202	var tcpPingVar float32
203	packet := MumbleProto.Ping{
204		Timestamp:  &timestamp,
205		TcpPackets: &c.tcpPacketsReceived,
206		TcpPingAvg: &tcpPingAvg,
207		TcpPingVar: &tcpPingVar,
208	}
209
210	t := time.Now()
211	for {
212		timestamp = uint64(t.UnixNano())
213		tcpPingAvg = math.Float32frombits(atomic.LoadUint32(&c.tcpPingAvg))
214		tcpPingVar = math.Float32frombits(atomic.LoadUint32(&c.tcpPingVar))
215		c.Conn.WriteProto(&packet)
216
217		select {
218		case <-c.end:
219			return
220		case t = <-ticker.C:
221			// continue to top of loop
222		}
223	}
224}
225
226// readRoutine reads protocol buffer messages from the server.
227func (c *Client) readRoutine() {
228	c.disconnectEvent = DisconnectEvent{
229		Client: c,
230		Type:   DisconnectError,
231	}
232
233	for {
234		pType, data, err := c.Conn.ReadPacket()
235		if err != nil {
236			break
237		}
238		if int(pType) < len(handlers) {
239			handlers[pType](c, data)
240		}
241	}
242
243	wasSynced := c.State() == StateSynced
244	atomic.StoreUint32(&c.state, uint32(StateDisconnected))
245	close(c.end)
246	if wasSynced {
247		c.Config.Listeners.onDisconnect(&c.disconnectEvent)
248	}
249}
250
251// RequestUserList requests that the server's registered user list be sent to
252// the client.
253func (c *Client) RequestUserList() {
254	packet := MumbleProto.UserList{}
255	c.Conn.WriteProto(&packet)
256}
257
258// RequestBanList requests that the server's ban list be sent to the client.
259func (c *Client) RequestBanList() {
260	packet := MumbleProto.BanList{
261		Query: proto.Bool(true),
262	}
263	c.Conn.WriteProto(&packet)
264}
265
266// Disconnect disconnects the client from the server.
267func (c *Client) Disconnect() error {
268	if c.State() == StateDisconnected {
269		return errors.New("gumble: client is already disconnected")
270	}
271	c.disconnectEvent.Type = DisconnectUser
272	c.Conn.Close()
273	return nil
274}
275
276// Do executes f in a thread-safe manner. It ensures that Client and its
277// associated data will not be changed during the lifetime of the function
278// call.
279func (c *Client) Do(f func()) {
280	c.volatile.RLock()
281	defer c.volatile.RUnlock()
282
283	f()
284}
285
286// Send will send a Message to the server.
287func (c *Client) Send(message Message) {
288	message.writeMessage(c)
289}
290