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: ×tamp, 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