forked from jshiffer/matterbridge
290 lines
7.2 KiB
Go
290 lines
7.2 KiB
Go
|
package gumble
|
||
|
|
||
|
import (
|
||
|
"crypto/tls"
|
||
|
"errors"
|
||
|
"math"
|
||
|
"net"
|
||
|
"runtime"
|
||
|
"sync/atomic"
|
||
|
"time"
|
||
|
|
||
|
"github.com/golang/protobuf/proto"
|
||
|
"layeh.com/gumble/gumble/MumbleProto"
|
||
|
)
|
||
|
|
||
|
// 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
|
||
|
|
||
|
// volatile is held by the client when the internal data structures are being
|
||
|
// modified.
|
||
|
volatile rpwMutex
|
||
|
|
||
|
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 timeout <-chan time.Time
|
||
|
{
|
||
|
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() {
|
||
|
timer := time.NewTimer(deadline.Sub(start))
|
||
|
defer timer.Stop()
|
||
|
timeout = timer.C
|
||
|
}
|
||
|
}
|
||
|
|
||
|
select {
|
||
|
case <-timeout:
|
||
|
client.Conn.Close()
|
||
|
return nil, errors.New("gumble: synchronization timeout")
|
||
|
case err := <-client.connect:
|
||
|
if 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.volatile.RLock()
|
||
|
defer c.volatile.RUnlock()
|
||
|
|
||
|
f()
|
||
|
}
|
||
|
|
||
|
// Send will send a Message to the server.
|
||
|
func (c *Client) Send(message Message) {
|
||
|
message.writeMessage(c)
|
||
|
}
|