├── .gitignore ├── misc ├── gumble.dia └── gumble.svg ├── gumble ├── MumbleProto │ ├── generate.go │ ├── generate_main.go │ └── LICENSE ├── detacher.go ├── contextactions.go ├── message.go ├── accesstokens.go ├── rpwmutex.go ├── varint │ ├── varint_test.go │ ├── read.go │ └── write.go ├── version.go ├── channels.go ├── permission.go ├── users.go ├── audiolisteners.go ├── textmessage.go ├── audiocodec.go ├── doc.go ├── userstats.go ├── config.go ├── contextaction.go ├── userlist.go ├── reject.go ├── voicetarget.go ├── audio.go ├── bans.go ├── ping.go ├── acl.go ├── listeners.go ├── conn.go ├── channel.go ├── user.go ├── event.go ├── client.go └── handlers.go ├── _examples └── mumble-audio-player │ ├── chimes.ogg │ └── main.go ├── gumbleutil ├── doc.go ├── channel.go ├── bitrate.go ├── textmessage.go ├── acl.go ├── main.go ├── listenerfunc.go └── listener.go ├── go.mod ├── go.sum ├── README.md ├── opus └── opus.go ├── cmd └── mumble-ping │ └── main.go ├── gumbleffmpeg ├── source.go └── stream.go ├── gumbleopenal └── stream.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | /.idea/ 3 | -------------------------------------------------------------------------------- /misc/gumble.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layeh/gumble/HEAD/misc/gumble.dia -------------------------------------------------------------------------------- /gumble/MumbleProto/generate.go: -------------------------------------------------------------------------------- 1 | //go:generate go run generate_main.go 2 | package MumbleProto 3 | -------------------------------------------------------------------------------- /_examples/mumble-audio-player/chimes.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/layeh/gumble/HEAD/_examples/mumble-audio-player/chimes.ogg -------------------------------------------------------------------------------- /gumbleutil/doc.go: -------------------------------------------------------------------------------- 1 | // Package gumbleutil provides extras that can make working with gumble easier. 2 | package gumbleutil 3 | -------------------------------------------------------------------------------- /gumble/detacher.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | // Detacher is an interface that event listeners implement. After the Detach 4 | // method is called, the listener will no longer receive events. 5 | type Detacher interface { 6 | Detach() 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module layeh.com/gumble 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372 7 | github.com/golang/protobuf v1.3.1 8 | layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa 9 | ) 10 | -------------------------------------------------------------------------------- /gumble/contextactions.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | // ContextActions is a map of ContextActions. 4 | type ContextActions map[string]*ContextAction 5 | 6 | func (c ContextActions) create(action string) *ContextAction { 7 | contextAction := &ContextAction{ 8 | Name: action, 9 | } 10 | c[action] = contextAction 11 | return contextAction 12 | } 13 | -------------------------------------------------------------------------------- /gumble/message.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | // Message is data that be encoded and sent to the server. The following 4 | // types implement this interface: 5 | // AccessTokens 6 | // ACL 7 | // BanList 8 | // RegisteredUsers 9 | // TextMessage 10 | // VoiceTarget 11 | type Message interface { 12 | writeMessage(client *Client) error 13 | } 14 | -------------------------------------------------------------------------------- /gumble/accesstokens.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "layeh.com/gumble/gumble/MumbleProto" 5 | ) 6 | 7 | // AccessTokens are additional passwords that can be provided to the server to 8 | // gain access to restricted channels. 9 | type AccessTokens []string 10 | 11 | func (a AccessTokens) writeMessage(client *Client) error { 12 | packet := MumbleProto.Authenticate{ 13 | Tokens: a, 14 | } 15 | return client.Conn.WriteProto(&packet) 16 | } 17 | -------------------------------------------------------------------------------- /gumbleutil/channel.go: -------------------------------------------------------------------------------- 1 | package gumbleutil 2 | 3 | import ( 4 | "layeh.com/gumble/gumble" 5 | ) 6 | 7 | // ChannelPath returns a slice of channel names, starting from the root channel 8 | // to the given channel. 9 | func ChannelPath(channel *gumble.Channel) []string { 10 | var pieces []string 11 | for ; channel != nil; channel = channel.Parent { 12 | pieces = append(pieces, channel.Name) 13 | } 14 | for i := 0; i < (len(pieces) / 2); i++ { 15 | pieces[len(pieces)-1-i], pieces[i] = pieces[i], pieces[len(pieces)-1-i] 16 | } 17 | return pieces 18 | } 19 | -------------------------------------------------------------------------------- /gumble/rpwmutex.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import "sync" 4 | 5 | // rpwMutex is a reader-preferred RWMutex. 6 | type rpwMutex struct { 7 | w sync.Mutex 8 | r sync.Mutex 9 | n int 10 | } 11 | 12 | func (m *rpwMutex) Lock() { 13 | m.w.Lock() 14 | } 15 | 16 | func (m *rpwMutex) Unlock() { 17 | m.w.Unlock() 18 | } 19 | 20 | func (m *rpwMutex) RLock() { 21 | m.r.Lock() 22 | m.n++ 23 | if m.n == 1 { 24 | m.w.Lock() 25 | } 26 | m.r.Unlock() 27 | } 28 | 29 | func (m *rpwMutex) RUnlock() { 30 | m.r.Lock() 31 | m.n-- 32 | if m.n == 0 { 33 | m.w.Unlock() 34 | } 35 | m.r.Unlock() 36 | } 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372 h1:tz3KnXWtRZR0RWOfcMNOw+HHezWLQa7vfSOWTtKjchI= 2 | github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372/go.mod h1:74z+CYu2/mx4N+mcIS/rsvfAxBPBV9uv8zRAnwyFkdI= 3 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 4 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 5 | layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa h1:WNU4LYsgD2UHxgKgB36mL6iMAMOvr127alafSlgBbiA= 6 | layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa/go.mod h1:AOef7vHz0+v4sWwJnr0jSyHiX/1NgsMoaxl+rEPz/I0= 7 | -------------------------------------------------------------------------------- /gumble/varint/varint_test.go: -------------------------------------------------------------------------------- 1 | package varint 2 | 3 | import "testing" 4 | 5 | func TestRange(t *testing.T) { 6 | 7 | fn := func(i int64) { 8 | var b [MaxVarintLen]byte 9 | size := Encode(b[:], i) 10 | if size == 0 { 11 | t.Error("Encode returned size 0\n") 12 | } 13 | s := b[:size] 14 | 15 | val, size := Decode(s) 16 | if size == 0 { 17 | t.Error("Decode return size 0\n") 18 | } 19 | 20 | if i != val { 21 | t.Errorf("Encoded %d (%v) equals decoded %d\n", i, s, val) 22 | } 23 | } 24 | 25 | for i := int64(-10000); i <= 10000; i++ { 26 | fn(i) 27 | } 28 | 29 | fn(134342525) 30 | fn(10282934828342) 31 | fn(1028293482834200000) 32 | } 33 | -------------------------------------------------------------------------------- /gumbleutil/bitrate.go: -------------------------------------------------------------------------------- 1 | package gumbleutil 2 | 3 | import ( 4 | "time" 5 | 6 | "layeh.com/gumble/gumble" 7 | ) 8 | 9 | var autoBitrate = &Listener{ 10 | Connect: func(e *gumble.ConnectEvent) { 11 | if e.MaximumBitrate != nil { 12 | const safety = 5 13 | interval := e.Client.Config.AudioInterval 14 | dataBytes := (*e.MaximumBitrate / (8 * (int(time.Second/interval) + safety))) - 32 - 10 15 | 16 | e.Client.Config.AudioDataBytes = dataBytes 17 | } 18 | }, 19 | } 20 | 21 | // AutoBitrate is a gumble.EventListener that automatically sets the client's 22 | // AudioDataBytes to suitable value, based on the server's bitrate. 23 | var AutoBitrate gumble.EventListener 24 | 25 | func init() { 26 | AutoBitrate = autoBitrate 27 | } 28 | -------------------------------------------------------------------------------- /gumble/version.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | // Version represents a Mumble client or server version. 4 | type Version struct { 5 | // The semantic version information as a single unsigned integer. 6 | // 7 | // Bits 0-15 are the major version, bits 16-23 are the minor version, and 8 | // bits 24-31 are the patch version. 9 | Version uint32 10 | // The name of the client. 11 | Release string 12 | // The operating system name. 13 | OS string 14 | // The operating system version. 15 | OSVersion string 16 | } 17 | 18 | // SemanticVersion returns the version's semantic version components. 19 | func (v *Version) SemanticVersion() (major uint16, minor, patch uint8) { 20 | major = uint16(v.Version>>16) & 0xFFFF 21 | minor = uint8(v.Version>>8) & 0xFF 22 | patch = uint8(v.Version) & 0xFF 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /gumble/channels.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | // Channels is a map of server channels. 4 | type Channels map[uint32]*Channel 5 | 6 | // create adds a new channel with the given id to the collection. If a channel 7 | // with the given id already exists, it is overwritten. 8 | func (c Channels) create(id uint32) *Channel { 9 | channel := &Channel{ 10 | ID: id, 11 | Links: Channels{}, 12 | Children: Channels{}, 13 | Users: Users{}, 14 | } 15 | c[id] = channel 16 | return channel 17 | } 18 | 19 | // Find returns a channel whose path (by channel name) from the server root 20 | // channel is equal to the arguments passed. nil is returned if c does not 21 | // containt the root channel. 22 | func (c Channels) Find(names ...string) *Channel { 23 | root := c[0] 24 | if names == nil || root == nil { 25 | return root 26 | } 27 | return root.Find(names...) 28 | } 29 | -------------------------------------------------------------------------------- /gumble/permission.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | // Permission is a bitmask of permissions given to a certain user. 4 | type Permission int 5 | 6 | // Permissions that can be applied in any channel. 7 | const ( 8 | PermissionWrite Permission = 1 << iota 9 | PermissionTraverse 10 | PermissionEnter 11 | PermissionSpeak 12 | PermissionMuteDeafen 13 | PermissionMove 14 | PermissionMakeChannel 15 | PermissionLinkChannel 16 | PermissionWhisper 17 | PermissionTextMessage 18 | PermissionMakeTemporaryChannel 19 | ) 20 | 21 | // Permissions that can only be applied in the root channel. 22 | const ( 23 | PermissionKick Permission = 0x10000 << iota 24 | PermissionBan 25 | PermissionRegister 26 | PermissionRegisterSelf 27 | ) 28 | 29 | // Has returns true if the Permission p contains Permission o has part of its 30 | // bitmask. 31 | func (p Permission) Has(o Permission) bool { 32 | return p&o == o 33 | } 34 | -------------------------------------------------------------------------------- /gumble/users.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | // Users is a map of server users. 4 | // 5 | // When accessed through client.Users, it contains all users currently on the 6 | // server. When accessed through a specific channel 7 | // (e.g. client.Channels[0].Users), it contains only the users in the 8 | // channel. 9 | type Users map[uint32]*User 10 | 11 | // create adds a new user with the given session to the collection. If a user 12 | // with the given session already exists, it is overwritten. 13 | func (u Users) create(session uint32) *User { 14 | user := &User{ 15 | Session: session, 16 | } 17 | u[session] = user 18 | return user 19 | } 20 | 21 | // Find returns the user with the given name. nil is returned if no user exists 22 | // with the given name. 23 | func (u Users) Find(name string) *User { 24 | for _, user := range u { 25 | if user.Name == name { 26 | return user 27 | } 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /gumble/audiolisteners.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | type audioEventItem struct { 4 | parent *AudioListeners 5 | prev, next *audioEventItem 6 | listener AudioListener 7 | streams map[*User]chan *AudioPacket 8 | } 9 | 10 | func (e *audioEventItem) Detach() { 11 | if e.prev == nil { 12 | e.parent.head = e.next 13 | } else { 14 | e.prev.next = e.next 15 | } 16 | if e.next == nil { 17 | e.parent.tail = e.prev 18 | } else { 19 | e.next.prev = e.prev 20 | } 21 | } 22 | 23 | // AudioListeners is a list of audio listeners. Each attached listener is 24 | // called in sequence when a new user audio stream begins. 25 | type AudioListeners struct { 26 | head, tail *audioEventItem 27 | } 28 | 29 | // Attach adds a new audio listener to the end of the current list of listeners. 30 | func (e *AudioListeners) Attach(listener AudioListener) Detacher { 31 | item := &audioEventItem{ 32 | parent: e, 33 | prev: e.tail, 34 | listener: listener, 35 | streams: make(map[*User]chan *AudioPacket), 36 | } 37 | if e.head == nil { 38 | e.head = item 39 | } 40 | if e.tail == nil { 41 | e.tail = item 42 | } else { 43 | e.tail.next = item 44 | } 45 | return item 46 | } 47 | -------------------------------------------------------------------------------- /gumbleutil/textmessage.go: -------------------------------------------------------------------------------- 1 | package gumbleutil 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "strings" 7 | 8 | "layeh.com/gumble/gumble" 9 | ) 10 | 11 | // PlainText returns the Message string without HTML tags or entities. 12 | func PlainText(tm *gumble.TextMessage) string { 13 | d := xml.NewDecoder(strings.NewReader(tm.Message)) 14 | d.Strict = false 15 | d.AutoClose = xml.HTMLAutoClose 16 | d.Entity = xml.HTMLEntity 17 | 18 | var b bytes.Buffer 19 | newline := false 20 | for { 21 | t, _ := d.Token() 22 | if t == nil { 23 | break 24 | } 25 | switch node := t.(type) { 26 | case xml.CharData: 27 | if len(node) > 0 { 28 | b.Write(node) 29 | newline = false 30 | } 31 | case xml.StartElement: 32 | switch node.Name.Local { 33 | case "address", "article", "aside", "audio", "blockquote", "canvas", "dd", "div", "dl", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "noscript", "ol", "output", "p", "pre", "section", "table", "tfoot", "ul", "video": 34 | if !newline { 35 | b.WriteByte('\n') 36 | newline = true 37 | } 38 | case "br": 39 | b.WriteByte('\n') 40 | newline = true 41 | } 42 | } 43 | } 44 | return b.String() 45 | } 46 | -------------------------------------------------------------------------------- /gumble/textmessage.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "layeh.com/gumble/gumble/MumbleProto" 5 | ) 6 | 7 | // TextMessage is a chat message that can be received from and sent to the 8 | // server. 9 | type TextMessage struct { 10 | // User who sent the message (can be nil). 11 | Sender *User 12 | // Users that receive the message. 13 | Users []*User 14 | // Channels that receive the message. 15 | Channels []*Channel 16 | // Channels that receive the message and send it recursively to sub-channels. 17 | Trees []*Channel 18 | // Chat message. 19 | Message string 20 | } 21 | 22 | func (t *TextMessage) writeMessage(client *Client) error { 23 | packet := MumbleProto.TextMessage{ 24 | Message: &t.Message, 25 | } 26 | if t.Users != nil { 27 | packet.Session = make([]uint32, len(t.Users)) 28 | for i, user := range t.Users { 29 | packet.Session[i] = user.Session 30 | } 31 | } 32 | if t.Channels != nil { 33 | packet.ChannelId = make([]uint32, len(t.Channels)) 34 | for i, channel := range t.Channels { 35 | packet.ChannelId[i] = channel.ID 36 | } 37 | } 38 | if t.Trees != nil { 39 | packet.TreeId = make([]uint32, len(t.Trees)) 40 | for i, channel := range t.Trees { 41 | packet.TreeId[i] = channel.ID 42 | } 43 | } 44 | return client.Conn.WriteProto(&packet) 45 | } 46 | -------------------------------------------------------------------------------- /gumble/MumbleProto/generate_main.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | ) 12 | 13 | func main() { 14 | // Get Mumble.proto 15 | resp, err := http.Get("https://raw.githubusercontent.com/mumble-voip/mumble/master/src/Mumble.proto") 16 | if err != nil { 17 | log.Fatalf("could not download Mumble.proto: %s\n", err) 18 | } 19 | defer resp.Body.Close() 20 | 21 | // Write Mumble.proto 22 | f, err := os.Create("Mumble.proto") 23 | if err != nil { 24 | log.Fatalf("could not create Mumble.proto: %s\n", err) 25 | } 26 | if _, err := io.Copy(f, resp.Body); err != nil { 27 | log.Fatalf("could not write Mumble.proto: %s\n", err) 28 | } 29 | if err := f.Close(); err != nil { 30 | log.Fatalf("could not close Mumble.proto: %s\n", err) 31 | } 32 | 33 | // Build proto-gen-go 34 | if err := exec.Command("go", "build", "-o", "protoc-gen-go", "github.com/golang/protobuf/protoc-gen-go").Run(); err != nil { 35 | log.Fatalf("could not build protoc-gen-go: %s\n", err) 36 | } 37 | 38 | // Generate code 39 | if err := exec.Command("protoc", "--go_out=.", "Mumble.proto").Run(); err != nil { 40 | log.Fatalf("could not run protoc: %s\n", err) 41 | } 42 | 43 | // Clean up 44 | os.Remove("Mumble.proto") 45 | os.Remove("protoc-gen-go") 46 | } 47 | -------------------------------------------------------------------------------- /_examples/mumble-audio-player/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "layeh.com/gumble/gumble" 10 | "layeh.com/gumble/gumbleffmpeg" 11 | "layeh.com/gumble/gumbleutil" 12 | _ "layeh.com/gumble/opus" 13 | ) 14 | 15 | func main() { 16 | files := make(map[string]string) 17 | var stream *gumbleffmpeg.Stream 18 | 19 | flag.Usage = func() { 20 | fmt.Fprintf(os.Stderr, "Usage of %s: [flags] [audio files...]\n", os.Args[0]) 21 | flag.PrintDefaults() 22 | } 23 | 24 | gumbleutil.Main(gumbleutil.AutoBitrate, gumbleutil.Listener{ 25 | Connect: func(e *gumble.ConnectEvent) { 26 | for _, file := range flag.Args() { 27 | key := filepath.Base(file) 28 | files[key] = file 29 | } 30 | 31 | fmt.Printf("audio player loaded! (%d files)\n", len(files)) 32 | }, 33 | 34 | TextMessage: func(e *gumble.TextMessageEvent) { 35 | if e.Sender == nil { 36 | return 37 | } 38 | file, ok := files[e.Message] 39 | if !ok { 40 | return 41 | } 42 | if stream != nil && stream.State() == gumbleffmpeg.StatePlaying { 43 | return 44 | } 45 | stream = gumbleffmpeg.New(e.Client, gumbleffmpeg.SourceFile(file)) 46 | if err := stream.Play(); err != nil { 47 | fmt.Printf("%s\n", err) 48 | } else { 49 | fmt.Printf("Playing %s\n", file) 50 | } 51 | }, 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /gumble/audiocodec.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | const ( 8 | audioCodecIDOpus = 4 9 | ) 10 | 11 | var ( 12 | audioCodecsLock sync.Mutex 13 | audioCodecs [8]AudioCodec 14 | ) 15 | 16 | // RegisterAudioCodec registers an audio codec that can be used for encoding 17 | // and decoding outgoing and incoming audio data. The function panics if the 18 | // ID is invalid. 19 | func RegisterAudioCodec(id int, codec AudioCodec) { 20 | audioCodecsLock.Lock() 21 | defer audioCodecsLock.Unlock() 22 | 23 | if id < 0 || id >= len(audioCodecs) { 24 | panic("id out of range") 25 | } 26 | audioCodecs[id] = codec 27 | } 28 | 29 | func getAudioCodec(id int) AudioCodec { 30 | audioCodecsLock.Lock() 31 | defer audioCodecsLock.Unlock() 32 | return audioCodecs[id] 33 | } 34 | 35 | // AudioCodec can create a encoder and a decoder for outgoing and incoming 36 | // data. 37 | type AudioCodec interface { 38 | ID() int 39 | NewEncoder() AudioEncoder 40 | NewDecoder() AudioDecoder 41 | } 42 | 43 | // AudioEncoder encodes a chunk of PCM audio samples to a certain type. 44 | type AudioEncoder interface { 45 | ID() int 46 | Encode(pcm []int16, mframeSize, maxDataBytes int) ([]byte, error) 47 | Reset() 48 | } 49 | 50 | // AudioDecoder decodes an encoded byte slice to a chunk of PCM audio samples. 51 | type AudioDecoder interface { 52 | ID() int 53 | Decode(data []byte, frameSize int) ([]int16, error) 54 | Reset() 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # gumble 4 | 5 | gumble is a [Mumble](https://mumble.info/) client implementation in Go 6 | 7 | ## Sub-projects 8 | 9 | - gumble ([docs](https://pkg.go.dev/layeh.com/gumble/gumble)) 10 | - Client library 11 | - gumbleopenal ([docs](https://pkg.go.dev/layeh.com/gumble/gumbleopenal)) 12 | - [OpenAL](http://kcat.strangesoft.net/openal.html) audio system for gumble 13 | - gumbleffmpeg ([docs](https://pkg.go.dev/layeh.com/gumble/gumbleffmpeg)) 14 | - [ffmpeg](https://www.ffmpeg.org/) audio source for gumble 15 | - gumbleutil ([docs](https://pkg.go.dev/layeh.com/gumble/gumbleutil)) 16 | - Extras that can make working with gumble easier 17 | 18 | ## Example 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "layeh.com/gumble/gumble" 25 | "layeh.com/gumble/gumbleutil" 26 | ) 27 | 28 | func main() { 29 | gumbleutil.Main(gumbleutil.Listener{ 30 | UserChange: func(e *gumble.UserChangeEvent) { 31 | if e.Type.Has(gumble.UserChangeConnected) { 32 | e.User.Send("Welcome to the server, " + e.User.Name + "!") 33 | } 34 | }, 35 | }) 36 | } 37 | ``` 38 | 39 | ## Related projects 40 | 41 | - [barnard](https://layeh.com/barnard) 42 | - terminal-based Mumble client 43 | - [piepan](https://layeh.com/piepan) 44 | - an easy to use framework for writing Mumble bots using Lua 45 | 46 | ## License 47 | 48 | MPL 2.0 49 | 50 | ## Author 51 | 52 | Tim Cooper () 53 | -------------------------------------------------------------------------------- /opus/opus.go: -------------------------------------------------------------------------------- 1 | package opus 2 | 3 | import ( 4 | "layeh.com/gopus" 5 | "layeh.com/gumble/gumble" 6 | ) 7 | 8 | var Codec gumble.AudioCodec 9 | 10 | const ID = 4 11 | 12 | func init() { 13 | Codec = &generator{} 14 | gumble.RegisterAudioCodec(4, Codec) 15 | } 16 | 17 | // generator 18 | 19 | type generator struct { 20 | } 21 | 22 | func (*generator) ID() int { 23 | return ID 24 | } 25 | 26 | func (*generator) NewEncoder() gumble.AudioEncoder { 27 | e, _ := gopus.NewEncoder(gumble.AudioSampleRate, gumble.AudioChannels, gopus.Voip) 28 | e.SetBitrate(gopus.BitrateMaximum) 29 | return &Encoder{ 30 | e, 31 | } 32 | } 33 | 34 | func (*generator) NewDecoder() gumble.AudioDecoder { 35 | d, _ := gopus.NewDecoder(gumble.AudioSampleRate, gumble.AudioChannels) 36 | return &Decoder{ 37 | d, 38 | } 39 | } 40 | 41 | // encoder 42 | 43 | type Encoder struct { 44 | *gopus.Encoder 45 | } 46 | 47 | func (*Encoder) ID() int { 48 | return ID 49 | } 50 | 51 | func (e *Encoder) Encode(pcm []int16, mframeSize, maxDataBytes int) ([]byte, error) { 52 | return e.Encoder.Encode(pcm, mframeSize, maxDataBytes) 53 | } 54 | 55 | func (e *Encoder) Reset() { 56 | e.Encoder.ResetState() 57 | } 58 | 59 | // decoder 60 | 61 | type Decoder struct { 62 | *gopus.Decoder 63 | } 64 | 65 | func (*Decoder) ID() int { 66 | return 4 67 | } 68 | 69 | func (d *Decoder) Decode(data []byte, frameSize int) ([]int16, error) { 70 | return d.Decoder.Decode(data, frameSize, false) 71 | } 72 | 73 | func (d *Decoder) Reset() { 74 | d.Decoder.ResetState() 75 | } 76 | -------------------------------------------------------------------------------- /gumble/doc.go: -------------------------------------------------------------------------------- 1 | // Package gumble is a client for the Mumble voice chat software. 2 | // 3 | // Getting started 4 | // 5 | // 1. Create a new Config to hold your connection settings: 6 | // 7 | // config := gumble.NewConfig() 8 | // config.Username = "gumble-test" 9 | // 10 | // 2. Attach event listeners to the configuration: 11 | // 12 | // config.Attach(gumbleutil.Listener{ 13 | // TextMessage: func(e *gumble.TextMessageEvent) { 14 | // fmt.Printf("Received text message: %s\n", e.Message) 15 | // }, 16 | // }) 17 | // 18 | // 3. Connect to the server: 19 | // 20 | // client, err := gumble.Dial("example.com:64738", config) 21 | // if err != nil { 22 | // panic(err) 23 | // } 24 | // 25 | // Audio codecs 26 | // 27 | // Currently, only the Opus codec (https://www.opus-codec.org/) is supported 28 | // for transmitting and receiving audio. It can be enabled by importing the 29 | // following package for its side effect: 30 | // import ( 31 | // _ "layeh.com/gumble/opus" 32 | // ) 33 | // 34 | // To ensure that gumble clients can always transmit and receive audio to and 35 | // from your server, add the following line to your murmur configuration file: 36 | // 37 | // opusthreshold=0 38 | // 39 | // Thread safety 40 | // 41 | // As a general rule, a Client everything that is associated with it 42 | // (Users, Channels, Config, etc.), is thread-unsafe. Accessing or modifying 43 | // those structures should only be done from inside of an event listener or via 44 | // Client.Do. 45 | package gumble 46 | -------------------------------------------------------------------------------- /gumble/varint/read.go: -------------------------------------------------------------------------------- 1 | package varint 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | // Decode reads the first varint encoded number from the given buffer. 8 | // 9 | // On success, the function returns the varint as an int64, and the number of 10 | // bytes read (0 if there was an error). 11 | func Decode(b []byte) (int64, int) { 12 | if len(b) == 0 { 13 | return 0, 0 14 | } 15 | // 0xxxxxxx 7-bit positive number 16 | if (b[0] & 0x80) == 0 { 17 | return int64(b[0]), 1 18 | } 19 | // 10xxxxxx + 1 byte 14-bit positive number 20 | if (b[0]&0xC0) == 0x80 && len(b) >= 2 { 21 | return int64(b[0]&0x3F)<<8 | int64(b[1]), 2 22 | } 23 | // 110xxxxx + 2 bytes 21-bit positive number 24 | if (b[0]&0xE0) == 0xC0 && len(b) >= 3 { 25 | return int64(b[0]&0x1F)<<16 | int64(b[1])<<8 | int64(b[2]), 3 26 | } 27 | // 1110xxxx + 3 bytes 28-bit positive number 28 | if (b[0]&0xF0) == 0xE0 && len(b) >= 4 { 29 | return int64(b[0]&0xF)<<24 | int64(b[1])<<16 | int64(b[2])<<8 | int64(b[3]), 4 30 | } 31 | // 111100__ + int (32-bit) 32-bit positive number 32 | if (b[0]&0xFC) == 0xF0 && len(b) >= 5 { 33 | return int64(binary.BigEndian.Uint32(b[1:])), 5 34 | } 35 | // 111101__ + long (64-bit) 64-bit number 36 | if (b[0]&0xFC) == 0xF4 && len(b) >= 9 { 37 | return int64(binary.BigEndian.Uint64(b[1:])), 9 38 | } 39 | // 111110__ + varint Negative recursive varint 40 | if b[0]&0xFC == 0xF8 { 41 | if v, n := Decode(b[1:]); n > 0 { 42 | return -v, n + 1 43 | } 44 | return 0, 0 45 | } 46 | // 111111xx Byte-inverted negative two bit number (~xx) 47 | if b[0]&0xFC == 0xFC { 48 | return ^int64(b[0] & 0x03), 1 49 | } 50 | 51 | return 0, 0 52 | } 53 | -------------------------------------------------------------------------------- /gumbleutil/acl.go: -------------------------------------------------------------------------------- 1 | package gumbleutil 2 | 3 | import ( 4 | "layeh.com/gumble/gumble" 5 | ) 6 | 7 | // UserGroups fetches the group names the given user belongs to in the given 8 | // channel. The slice of group names sent via the returned channel. On error, 9 | // the returned channel is closed without without sending a slice. 10 | func UserGroups(client *gumble.Client, user *gumble.User, channel *gumble.Channel) <-chan []string { 11 | ch := make(chan []string) 12 | 13 | if !user.IsRegistered() { 14 | close(ch) 15 | return ch 16 | } 17 | 18 | var detacher gumble.Detacher 19 | listener := Listener{ 20 | Disconnect: func(e *gumble.DisconnectEvent) { 21 | detacher.Detach() 22 | close(ch) 23 | }, 24 | ChannelChange: func(e *gumble.ChannelChangeEvent) { 25 | if e.Channel == channel && e.Type.Has(gumble.ChannelChangeRemoved) { 26 | detacher.Detach() 27 | close(ch) 28 | } 29 | }, 30 | PermissionDenied: func(e *gumble.PermissionDeniedEvent) { 31 | if e.Channel == channel && e.Type == gumble.PermissionDeniedPermission && (e.Permission&gumble.PermissionWrite) != 0 { 32 | detacher.Detach() 33 | close(ch) 34 | } 35 | }, 36 | ACL: func(e *gumble.ACLEvent) { 37 | if e.ACL.Channel != channel { 38 | return 39 | } 40 | var names []string 41 | for _, g := range e.ACL.Groups { 42 | if (g.UsersAdd[user.UserID] != nil || g.UsersInherited[user.UserID] != nil) && g.UsersRemove[user.UserID] == nil { 43 | names = append(names, g.Name) 44 | } 45 | } 46 | detacher.Detach() 47 | ch <- names 48 | close(ch) 49 | }, 50 | } 51 | detacher = client.Config.Attach(&listener) 52 | channel.RequestACL() 53 | 54 | return ch 55 | } 56 | -------------------------------------------------------------------------------- /gumble/userstats.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "crypto/x509" 5 | "net" 6 | "time" 7 | ) 8 | 9 | // UserStats contains additional information about a user. 10 | type UserStats struct { 11 | // The owner of the stats. 12 | User *User 13 | 14 | // Stats about UDP packets sent from the client. 15 | FromClient UserStatsUDP 16 | // Stats about UDP packets sent by the server. 17 | FromServer UserStatsUDP 18 | 19 | // Number of UDP packets sent by the user. 20 | UDPPackets uint32 21 | // Average UDP ping. 22 | UDPPingAverage float32 23 | // UDP ping variance. 24 | UDPPingVariance float32 25 | 26 | // Number of TCP packets sent by the user. 27 | TCPPackets uint32 28 | // Average TCP ping. 29 | TCPPingAverage float32 30 | // TCP ping variance. 31 | TCPPingVariance float32 32 | 33 | // The user's version. 34 | Version Version 35 | // When the user connected to the server. 36 | Connected time.Time 37 | // How long the user has been idle. 38 | Idle time.Duration 39 | // How much bandwidth the user is current using. 40 | Bandwidth int 41 | // The user's certificate chain. 42 | Certificates []*x509.Certificate 43 | // Does the user have a strong certificate? A strong certificate is one that 44 | // is not self signed, nor expired, etc. 45 | StrongCertificate bool 46 | // A list of CELT versions supported by the user's client. 47 | CELTVersions []int32 48 | // Does the user's client supports the Opus audio codec? 49 | Opus bool 50 | 51 | // The user's IP address. 52 | IP net.IP 53 | } 54 | 55 | // UserStatsUDP contains stats about UDP packets that have been sent to or from 56 | // the server. 57 | type UserStatsUDP struct { 58 | Good uint32 59 | Late uint32 60 | Lost uint32 61 | Resync uint32 62 | } 63 | -------------------------------------------------------------------------------- /gumble/varint/write.go: -------------------------------------------------------------------------------- 1 | package varint 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | ) 7 | 8 | // MaxVarintLen is the maximum number of bytes required to encode a varint 9 | // number. 10 | const MaxVarintLen = 10 11 | 12 | // Encode encodes the given value to varint format. 13 | func Encode(b []byte, value int64) int { 14 | // 111111xx Byte-inverted negative two bit number (~xx) 15 | if value <= -1 && value >= -4 { 16 | b[0] = 0xFC | byte(^value&0xFF) 17 | return 1 18 | } 19 | // 111110__ + varint Negative recursive varint 20 | if value < 0 { 21 | b[0] = 0xF8 22 | return 1 + Encode(b[1:], -value) 23 | } 24 | // 0xxxxxxx 7-bit positive number 25 | if value <= 0x7F { 26 | b[0] = byte(value) 27 | return 1 28 | } 29 | // 10xxxxxx + 1 byte 14-bit positive number 30 | if value <= 0x3FFF { 31 | b[0] = byte(((value >> 8) & 0x3F) | 0x80) 32 | b[1] = byte(value & 0xFF) 33 | return 2 34 | } 35 | // 110xxxxx + 2 bytes 21-bit positive number 36 | if value <= 0x1FFFFF { 37 | b[0] = byte((value>>16)&0x1F | 0xC0) 38 | b[1] = byte((value >> 8) & 0xFF) 39 | b[2] = byte(value & 0xFF) 40 | return 3 41 | } 42 | // 1110xxxx + 3 bytes 28-bit positive number 43 | if value <= 0xFFFFFFF { 44 | b[0] = byte((value>>24)&0xF | 0xE0) 45 | b[1] = byte((value >> 16) & 0xFF) 46 | b[2] = byte((value >> 8) & 0xFF) 47 | b[3] = byte(value & 0xFF) 48 | return 4 49 | } 50 | // 111100__ + int (32-bit) 32-bit positive number 51 | if value <= math.MaxInt32 { 52 | b[0] = 0xF0 53 | binary.BigEndian.PutUint32(b[1:], uint32(value)) 54 | return 5 55 | } 56 | // 111101__ + long (64-bit) 64-bit number 57 | if value <= math.MaxInt64 { 58 | b[0] = 0xF4 59 | binary.BigEndian.PutUint64(b[1:], uint64(value)) 60 | return 9 61 | } 62 | 63 | return 0 64 | } 65 | -------------------------------------------------------------------------------- /gumble/config.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Config holds the Mumble configuration used by Client. A single Config should 8 | // not be shared between multiple Client instances. 9 | type Config struct { 10 | // User name used when authenticating with the server. 11 | Username string 12 | // Password used when authenticating with the server. A password is not 13 | // usually required to connect to a server. 14 | Password string 15 | // The initial access tokens to the send to the server. Access tokens can be 16 | // resent to the server using: 17 | // client.Send(config.Tokens) 18 | Tokens AccessTokens 19 | 20 | // AudioInterval is the interval at which audio packets are sent. Valid 21 | // values are: 10ms, 20ms, 40ms, and 60ms. 22 | AudioInterval time.Duration 23 | // AudioDataBytes is the number of bytes that an audio frame can use. 24 | AudioDataBytes int 25 | 26 | // The event listeners used when client events are triggered. 27 | Listeners Listeners 28 | AudioListeners AudioListeners 29 | } 30 | 31 | // NewConfig returns a new Config struct with default values set. 32 | func NewConfig() *Config { 33 | return &Config{ 34 | AudioInterval: AudioDefaultInterval, 35 | AudioDataBytes: AudioDefaultDataBytes, 36 | } 37 | } 38 | 39 | // Attach is an alias of c.Listeners.Attach. 40 | func (c *Config) Attach(l EventListener) Detacher { 41 | return c.Listeners.Attach(l) 42 | } 43 | 44 | // AttachAudio is an alias of c.AudioListeners.Attach. 45 | func (c *Config) AttachAudio(l AudioListener) Detacher { 46 | return c.AudioListeners.Attach(l) 47 | } 48 | 49 | // AudioFrameSize returns the appropriate audio frame size, based off of the 50 | // audio interval. 51 | func (c *Config) AudioFrameSize() int { 52 | return int(c.AudioInterval/AudioDefaultInterval) * AudioDefaultFrameSize 53 | } 54 | -------------------------------------------------------------------------------- /gumble/contextaction.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "layeh.com/gumble/gumble/MumbleProto" 5 | ) 6 | 7 | // ContextActionType is a bitmask of contexts where a ContextAction can be 8 | // triggered. 9 | type ContextActionType int 10 | 11 | // Supported ContextAction contexts. 12 | const ( 13 | ContextActionServer ContextActionType = ContextActionType(MumbleProto.ContextActionModify_Server) 14 | ContextActionChannel ContextActionType = ContextActionType(MumbleProto.ContextActionModify_Channel) 15 | ContextActionUser ContextActionType = ContextActionType(MumbleProto.ContextActionModify_User) 16 | ) 17 | 18 | // ContextAction is an triggerable item that has been added by a server-side 19 | // plugin. 20 | type ContextAction struct { 21 | // The context action type. 22 | Type ContextActionType 23 | // The name of the context action. 24 | Name string 25 | // The user-friendly description of the context action. 26 | Label string 27 | 28 | client *Client 29 | } 30 | 31 | // Trigger will trigger the context action in the context of the server. 32 | func (c *ContextAction) Trigger() { 33 | packet := MumbleProto.ContextAction{ 34 | Action: &c.Name, 35 | } 36 | c.client.Conn.WriteProto(&packet) 37 | } 38 | 39 | // TriggerUser will trigger the context action in the context of the given 40 | // user. 41 | func (c *ContextAction) TriggerUser(user *User) { 42 | packet := MumbleProto.ContextAction{ 43 | Session: &user.Session, 44 | Action: &c.Name, 45 | } 46 | c.client.Conn.WriteProto(&packet) 47 | } 48 | 49 | // TriggerChannel will trigger the context action in the context of the given 50 | // channel. 51 | func (c *ContextAction) TriggerChannel(channel *Channel) { 52 | packet := MumbleProto.ContextAction{ 53 | ChannelId: &channel.ID, 54 | Action: &c.Name, 55 | } 56 | c.client.Conn.WriteProto(&packet) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/mumble-ping/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "strconv" 10 | "time" 11 | 12 | "layeh.com/gumble/gumble" 13 | ) 14 | 15 | func main() { 16 | flag.Usage = func() { 17 | fmt.Fprintf(os.Stderr, "usage: %s [flags] \n", os.Args[0]) 18 | flag.PrintDefaults() 19 | } 20 | interval := flag.Duration("interval", time.Second*1, "ping packet retransmission interval") 21 | timeout := flag.Duration("timeout", time.Second*5, "ping timeout until failure") 22 | jsonOutput := flag.Bool("json", false, "output success response as JSON") 23 | flag.Parse() 24 | if flag.NArg() != 1 { 25 | flag.Usage() 26 | os.Exit(1) 27 | } 28 | 29 | server := flag.Arg(0) 30 | 31 | host, port, err := net.SplitHostPort(server) 32 | if err != nil { 33 | host = server 34 | port = strconv.Itoa(gumble.DefaultPort) 35 | } 36 | 37 | resp, err := gumble.Ping(net.JoinHostPort(host, port), *interval, *timeout) 38 | if err != nil { 39 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 40 | os.Exit(1) 41 | } 42 | major, minor, patch := resp.Version.SemanticVersion() 43 | 44 | if !*jsonOutput { 45 | fmt.Printf("Address: %s\n", resp.Address) 46 | fmt.Printf("Ping: %s\n", resp.Ping) 47 | fmt.Printf("Version: %d.%d.%d\n", major, minor, patch) 48 | fmt.Printf("Connected Users: %d\n", resp.ConnectedUsers) 49 | fmt.Printf("Maximum Users: %d\n", resp.MaximumUsers) 50 | fmt.Printf("Maximum Bitrate: %d\n", resp.MaximumBitrate) 51 | } else { 52 | output := map[string]interface{}{ 53 | "address": resp.Address.String(), 54 | "ping": float64(resp.Ping) / float64(time.Millisecond), 55 | "version": fmt.Sprintf("%d.%d.%d", major, minor, patch), 56 | "connected_users": resp.ConnectedUsers, 57 | "maximum_users": resp.MaximumUsers, 58 | "maximum_bitrate": resp.MaximumBitrate, 59 | } 60 | encoder := json.NewEncoder(os.Stdout) 61 | encoder.Encode(output) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /gumble/userlist.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "time" 5 | 6 | "layeh.com/gumble/gumble/MumbleProto" 7 | ) 8 | 9 | // RegisteredUser represents a registered user on the server. 10 | type RegisteredUser struct { 11 | // The registered user's ID. 12 | UserID uint32 13 | // The registered user's name. 14 | Name string 15 | // The last time the user was seen by the server. 16 | LastSeen time.Time 17 | // The last channel the user was seen in. 18 | LastChannel *Channel 19 | 20 | changed bool 21 | deregister bool 22 | } 23 | 24 | // SetName sets the new name for the user. 25 | func (r *RegisteredUser) SetName(name string) { 26 | r.Name = name 27 | r.changed = true 28 | } 29 | 30 | // Deregister will remove the registered user from the server. 31 | func (r *RegisteredUser) Deregister() { 32 | r.deregister = true 33 | } 34 | 35 | // Register will keep the user registered on the server. This is only useful if 36 | // Deregister() was called on the registered user. 37 | func (r *RegisteredUser) Register() { 38 | r.deregister = false 39 | } 40 | 41 | // ACLUser returns an ACLUser for the given registered user. 42 | func (r *RegisteredUser) ACLUser() *ACLUser { 43 | return &ACLUser{ 44 | UserID: r.UserID, 45 | Name: r.Name, 46 | } 47 | } 48 | 49 | // RegisteredUsers is a list of users who are registered on the server. 50 | // 51 | // Whenever a registered user is changed, it does not come into effect until 52 | // the registered user list is sent back to the server. 53 | type RegisteredUsers []*RegisteredUser 54 | 55 | func (r RegisteredUsers) writeMessage(client *Client) error { 56 | packet := MumbleProto.UserList{} 57 | 58 | for _, user := range r { 59 | if user.deregister || user.changed { 60 | userListUser := &MumbleProto.UserList_User{ 61 | UserId: &user.UserID, 62 | } 63 | if !user.deregister { 64 | userListUser.Name = &user.Name 65 | } 66 | packet.Users = append(packet.Users, userListUser) 67 | } 68 | } 69 | 70 | if len(packet.Users) <= 0 { 71 | return nil 72 | } 73 | return client.Conn.WriteProto(&packet) 74 | } 75 | -------------------------------------------------------------------------------- /gumble/reject.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "strconv" 5 | 6 | "layeh.com/gumble/gumble/MumbleProto" 7 | ) 8 | 9 | // RejectType describes why a client connection was rejected by the server. 10 | type RejectType int 11 | 12 | // The possible reason why a client connection was rejected by the server. 13 | const ( 14 | RejectNone RejectType = RejectType(MumbleProto.Reject_None) 15 | RejectVersion RejectType = RejectType(MumbleProto.Reject_WrongVersion) 16 | RejectUserName RejectType = RejectType(MumbleProto.Reject_InvalidUsername) 17 | RejectUserCredentials RejectType = RejectType(MumbleProto.Reject_WrongUserPW) 18 | RejectServerPassword RejectType = RejectType(MumbleProto.Reject_WrongServerPW) 19 | RejectUsernameInUse RejectType = RejectType(MumbleProto.Reject_UsernameInUse) 20 | RejectServerFull RejectType = RejectType(MumbleProto.Reject_ServerFull) 21 | RejectNoCertificate RejectType = RejectType(MumbleProto.Reject_NoCertificate) 22 | RejectAuthenticatorFail RejectType = RejectType(MumbleProto.Reject_AuthenticatorFail) 23 | ) 24 | 25 | // RejectError is returned by DialWithDialer when the server rejects the client 26 | // connection. 27 | type RejectError struct { 28 | Type RejectType 29 | Reason string 30 | } 31 | 32 | // Error implements error. 33 | func (e RejectError) Error() string { 34 | var msg string 35 | switch e.Type { 36 | case RejectNone: 37 | msg = "none" 38 | case RejectVersion: 39 | msg = "wrong client version" 40 | case RejectUserName: 41 | msg = "invalid username" 42 | case RejectUserCredentials: 43 | msg = "incorrect user credentials" 44 | case RejectServerPassword: 45 | msg = "incorrect server password" 46 | case RejectUsernameInUse: 47 | msg = "username in use" 48 | case RejectServerFull: 49 | msg = "server full" 50 | case RejectNoCertificate: 51 | msg = "no certificate" 52 | case RejectAuthenticatorFail: 53 | msg = "authenticator fail" 54 | default: 55 | msg = "unknown type " + strconv.Itoa(int(e.Type)) 56 | } 57 | if e.Reason != "" { 58 | msg += ": " + e.Reason 59 | } 60 | return msg 61 | } 62 | -------------------------------------------------------------------------------- /gumble/MumbleProto/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2005-2013, Thorvald Natvig 2 | Copyright (C) 2007, Stefan Gehn 3 | Copyright (C) 2007, Sebastian Schlingmann 4 | Copyright (C) 2008-2013, Mikkel Krautz 5 | Copyright (C) 2008, Andreas Messer 6 | Copyright (C) 2007, Trenton Schulz 7 | Copyright (C) 2008-2013, Stefan Hacker 8 | Copyright (C) 2008-2011, Snares 9 | Copyright (C) 2009-2013, Benjamin Jemlich 10 | Copyright (C) 2009-2013, Kissaki 11 | 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions 16 | are met: 17 | 18 | - Redistributions of source code must retain the above copyright notice, 19 | this list of conditions and the following disclaimer. 20 | - Redistributions in binary form must reproduce the above copyright notice, 21 | this list of conditions and the following disclaimer in the documentation 22 | and/or other materials provided with the distribution. 23 | - Neither the name of the Mumble Developers nor the names of its 24 | contributors may be used to endorse or promote products derived from this 25 | software without specific prior written permission. 26 | 27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 28 | `AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 29 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 30 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR 31 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 32 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 33 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 34 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 35 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 36 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 37 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 38 | -------------------------------------------------------------------------------- /gumbleffmpeg/source.go: -------------------------------------------------------------------------------- 1 | package gumbleffmpeg 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | ) 7 | 8 | // Source is a Stream source. 9 | type Source interface { 10 | // must include the -i 11 | arguments() []string 12 | start(*exec.Cmd) error 13 | done() 14 | } 15 | 16 | // sourceFile 17 | 18 | type sourceFile string 19 | 20 | // SourceFile is standard file source. 21 | func SourceFile(filename string) Source { 22 | return sourceFile(filename) 23 | } 24 | 25 | func (s sourceFile) arguments() []string { 26 | return []string{"-i", string(s)} 27 | } 28 | 29 | func (sourceFile) start(*exec.Cmd) error { 30 | return nil 31 | } 32 | 33 | func (sourceFile) done() { 34 | } 35 | 36 | // sourceReader 37 | 38 | type sourceReader struct { 39 | r io.ReadCloser 40 | } 41 | 42 | // SourceReader is a ReadCloser source. 43 | func SourceReader(r io.ReadCloser) Source { 44 | return &sourceReader{r} 45 | } 46 | 47 | func (*sourceReader) arguments() []string { 48 | return []string{"-i", "-"} 49 | } 50 | 51 | func (s *sourceReader) start(cmd *exec.Cmd) error { 52 | cmd.Stdin = s.r 53 | return nil 54 | } 55 | 56 | func (s *sourceReader) done() { 57 | s.r.Close() 58 | } 59 | 60 | // sourceExec 61 | 62 | type sourceExec struct { 63 | name string 64 | arg []string 65 | 66 | cmd *exec.Cmd 67 | } 68 | 69 | // SourceExec uses the output of the given command and arguments as source 70 | // data. 71 | func SourceExec(name string, arg ...string) Source { 72 | return &sourceExec{ 73 | name: name, 74 | arg: arg, 75 | } 76 | } 77 | 78 | func (*sourceExec) arguments() []string { 79 | return []string{"-i", "-"} 80 | } 81 | 82 | func (s *sourceExec) start(cmd *exec.Cmd) error { 83 | s.cmd = exec.Command(s.name, s.arg...) 84 | r, err := s.cmd.StdoutPipe() 85 | if err != nil { 86 | return err 87 | } 88 | cmd.Stdin = r 89 | if err := s.cmd.Start(); err != nil { 90 | cmd.Stdin = nil 91 | return err 92 | } 93 | return nil 94 | } 95 | 96 | func (s *sourceExec) done() { 97 | if s.cmd != nil { 98 | if p := s.cmd.Process; p != nil { 99 | p.Kill() 100 | } 101 | s.cmd.Wait() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /gumbleutil/main.go: -------------------------------------------------------------------------------- 1 | package gumbleutil 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "os" 9 | "strconv" 10 | 11 | "layeh.com/gumble/gumble" 12 | ) 13 | 14 | // Main aids in the creation of a basic command line gumble bot. It accepts the 15 | // following flag arguments: 16 | // --server 17 | // --username 18 | // --password 19 | // --insecure 20 | // --certificate 21 | // --key 22 | func Main(listeners ...gumble.EventListener) { 23 | server := flag.String("server", "localhost:64738", "Mumble server address") 24 | username := flag.String("username", "gumble-bot", "client username") 25 | password := flag.String("password", "", "client password") 26 | insecure := flag.Bool("insecure", false, "skip server certificate verification") 27 | certificateFile := flag.String("certificate", "", "user certificate file (PEM)") 28 | keyFile := flag.String("key", "", "user certificate key file (PEM)") 29 | 30 | if !flag.Parsed() { 31 | flag.Parse() 32 | } 33 | 34 | host, port, err := net.SplitHostPort(*server) 35 | if err != nil { 36 | host = *server 37 | port = strconv.Itoa(gumble.DefaultPort) 38 | } 39 | 40 | keepAlive := make(chan bool) 41 | 42 | config := gumble.NewConfig() 43 | config.Username = *username 44 | config.Password = *password 45 | address := net.JoinHostPort(host, port) 46 | 47 | var tlsConfig tls.Config 48 | 49 | if *insecure { 50 | tlsConfig.InsecureSkipVerify = true 51 | } 52 | if *certificateFile != "" { 53 | if *keyFile == "" { 54 | keyFile = certificateFile 55 | } 56 | if certificate, err := tls.LoadX509KeyPair(*certificateFile, *keyFile); err != nil { 57 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 58 | os.Exit(1) 59 | } else { 60 | tlsConfig.Certificates = append(tlsConfig.Certificates, certificate) 61 | } 62 | } 63 | config.Attach(AutoBitrate) 64 | for _, listener := range listeners { 65 | config.Attach(listener) 66 | } 67 | config.Attach(Listener{ 68 | Disconnect: func(e *gumble.DisconnectEvent) { 69 | keepAlive <- true 70 | }, 71 | }) 72 | _, err = gumble.DialWithDialer(new(net.Dialer), address, config, &tlsConfig) 73 | if err != nil { 74 | fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) 75 | os.Exit(1) 76 | } 77 | 78 | <-keepAlive 79 | } 80 | -------------------------------------------------------------------------------- /gumble/voicetarget.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "layeh.com/gumble/gumble/MumbleProto" 5 | ) 6 | 7 | // VoiceTargetLoopback is a special voice target which causes any audio sent to 8 | // the server to be returned to the client. 9 | // 10 | // Its ID should not be modified, and it does not have to to be sent to the 11 | // server before use. 12 | var VoiceTargetLoopback *VoiceTarget = &VoiceTarget{ 13 | ID: 31, 14 | } 15 | 16 | type voiceTargetChannel struct { 17 | channel *Channel 18 | links, recursive bool 19 | group string 20 | } 21 | 22 | // VoiceTarget represents a set of users and/or channels that the client can 23 | // whisper to. 24 | type VoiceTarget struct { 25 | // The voice target ID. This value must be in the range [1, 30]. 26 | ID uint32 27 | users []*User 28 | channels []*voiceTargetChannel 29 | } 30 | 31 | // Clear removes all users and channels from the voice target. 32 | func (v *VoiceTarget) Clear() { 33 | v.users = nil 34 | v.channels = nil 35 | } 36 | 37 | // AddUser adds a user to the voice target. 38 | func (v *VoiceTarget) AddUser(user *User) { 39 | v.users = append(v.users, user) 40 | } 41 | 42 | // AddChannel adds a user to the voice target. If group is non-empty, only 43 | // users belonging to that ACL group will be targeted. 44 | func (v *VoiceTarget) AddChannel(channel *Channel, recursive, links bool, group string) { 45 | v.channels = append(v.channels, &voiceTargetChannel{ 46 | channel: channel, 47 | links: links, 48 | recursive: recursive, 49 | group: group, 50 | }) 51 | } 52 | 53 | func (v *VoiceTarget) writeMessage(client *Client) error { 54 | packet := MumbleProto.VoiceTarget{ 55 | Id: &v.ID, 56 | Targets: make([]*MumbleProto.VoiceTarget_Target, 0, len(v.users)+len(v.channels)), 57 | } 58 | for _, user := range v.users { 59 | packet.Targets = append(packet.Targets, &MumbleProto.VoiceTarget_Target{ 60 | Session: []uint32{user.Session}, 61 | }) 62 | } 63 | for _, vtChannel := range v.channels { 64 | target := &MumbleProto.VoiceTarget_Target{ 65 | ChannelId: &vtChannel.channel.ID, 66 | Links: &vtChannel.links, 67 | Children: &vtChannel.recursive, 68 | } 69 | if vtChannel.group != "" { 70 | target.Group = &vtChannel.group 71 | } 72 | packet.Targets = append(packet.Targets, target) 73 | } 74 | 75 | return client.Conn.WriteProto(&packet) 76 | } 77 | -------------------------------------------------------------------------------- /gumbleutil/listenerfunc.go: -------------------------------------------------------------------------------- 1 | package gumbleutil 2 | 3 | import ( 4 | "layeh.com/gumble/gumble" 5 | ) 6 | 7 | // ListenerFunc is a single listener function that implements the 8 | // gumble.EventListener interface. This is useful if you would like to use a 9 | // type-switch for handling the different event types. 10 | // 11 | // Example: 12 | // handler := func(e interface{}) { 13 | // switch e.(type) { 14 | // case *gumble.ConnectEvent: 15 | // println("Connected") 16 | // case *gumble.DisconnectEvent: 17 | // println("Disconnected") 18 | // // ... 19 | // } 20 | // } 21 | // 22 | // client.Attach(gumbleutil.ListenerFunc(handler)) 23 | type ListenerFunc func(e interface{}) 24 | 25 | var _ gumble.EventListener = ListenerFunc(nil) 26 | 27 | // OnConnect implements gumble.EventListener.OnConnect. 28 | func (lf ListenerFunc) OnConnect(e *gumble.ConnectEvent) { 29 | lf(e) 30 | } 31 | 32 | // OnDisconnect implements gumble.EventListener.OnDisconnect. 33 | func (lf ListenerFunc) OnDisconnect(e *gumble.DisconnectEvent) { 34 | lf(e) 35 | } 36 | 37 | // OnTextMessage implements gumble.EventListener.OnTextMessage. 38 | func (lf ListenerFunc) OnTextMessage(e *gumble.TextMessageEvent) { 39 | lf(e) 40 | } 41 | 42 | // OnUserChange implements gumble.EventListener.OnUserChange. 43 | func (lf ListenerFunc) OnUserChange(e *gumble.UserChangeEvent) { 44 | lf(e) 45 | } 46 | 47 | // OnChannelChange implements gumble.EventListener.OnChannelChange. 48 | func (lf ListenerFunc) OnChannelChange(e *gumble.ChannelChangeEvent) { 49 | lf(e) 50 | } 51 | 52 | // OnPermissionDenied implements gumble.EventListener.OnPermissionDenied. 53 | func (lf ListenerFunc) OnPermissionDenied(e *gumble.PermissionDeniedEvent) { 54 | lf(e) 55 | } 56 | 57 | // OnUserList implements gumble.EventListener.OnUserList. 58 | func (lf ListenerFunc) OnUserList(e *gumble.UserListEvent) { 59 | lf(e) 60 | } 61 | 62 | // OnACL implements gumble.EventListener.OnACL. 63 | func (lf ListenerFunc) OnACL(e *gumble.ACLEvent) { 64 | lf(e) 65 | } 66 | 67 | // OnBanList implements gumble.EventListener.OnBanList. 68 | func (lf ListenerFunc) OnBanList(e *gumble.BanListEvent) { 69 | lf(e) 70 | } 71 | 72 | // OnContextActionChange implements gumble.EventListener.OnContextActionChange. 73 | func (lf ListenerFunc) OnContextActionChange(e *gumble.ContextActionChangeEvent) { 74 | lf(e) 75 | } 76 | 77 | // OnServerConfig implements gumble.EventListener.OnServerConfig. 78 | func (lf ListenerFunc) OnServerConfig(e *gumble.ServerConfigEvent) { 79 | lf(e) 80 | } 81 | -------------------------------------------------------------------------------- /gumble/audio.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | // AudioSampleRate is the audio sample rate (in hertz) for incoming and 9 | // outgoing audio. 10 | AudioSampleRate = 48000 11 | 12 | // AudioDefaultInterval is the default interval that audio packets are sent 13 | // at. 14 | AudioDefaultInterval = 10 * time.Millisecond 15 | 16 | // AudioDefaultFrameSize is the number of audio frames that should be sent in 17 | // a 10ms window. 18 | AudioDefaultFrameSize = AudioSampleRate / 100 19 | 20 | // AudioMaximumFrameSize is the maximum audio frame size from another user 21 | // that will be processed. 22 | AudioMaximumFrameSize = AudioSampleRate / 1000 * 60 23 | 24 | // AudioDefaultDataBytes is the default number of bytes that an audio frame 25 | // can use. 26 | AudioDefaultDataBytes = 40 27 | 28 | // AudioChannels is the number of audio channels that are contained in an 29 | // audio stream. 30 | AudioChannels = 1 31 | ) 32 | 33 | // AudioListener is the interface that must be implemented by types wishing to 34 | // receive incoming audio data from the server. 35 | // 36 | // OnAudioStream is called when an audio stream for a user starts. It is the 37 | // implementer's responsibility to continuously process AudioStreamEvent.C 38 | // until it is closed. 39 | type AudioListener interface { 40 | OnAudioStream(e *AudioStreamEvent) 41 | } 42 | 43 | // AudioStreamEvent is event that is passed to AudioListener.OnAudioStream. 44 | type AudioStreamEvent struct { 45 | Client *Client 46 | User *User 47 | C <-chan *AudioPacket 48 | } 49 | 50 | // AudioBuffer is a slice of PCM audio samples. 51 | type AudioBuffer []int16 52 | 53 | func (a AudioBuffer) writeAudio(client *Client, seq int64, final bool) error { 54 | encoder := client.AudioEncoder 55 | if encoder == nil { 56 | return nil 57 | } 58 | dataBytes := client.Config.AudioDataBytes 59 | raw, err := encoder.Encode(a, len(a), dataBytes) 60 | if final { 61 | defer encoder.Reset() 62 | } 63 | if err != nil { 64 | return err 65 | } 66 | 67 | var targetID byte 68 | if target := client.VoiceTarget; target != nil { 69 | targetID = byte(target.ID) 70 | } 71 | // TODO: re-enable positional audio 72 | return client.Conn.WriteAudio(byte(4), targetID, seq, final, raw, nil, nil, nil) 73 | } 74 | 75 | // AudioPacket contains incoming audio samples and information. 76 | type AudioPacket struct { 77 | Client *Client 78 | Sender *User 79 | Target *VoiceTarget 80 | 81 | AudioBuffer 82 | 83 | HasPosition bool 84 | X, Y, Z float32 85 | } 86 | -------------------------------------------------------------------------------- /gumble/bans.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/golang/protobuf/proto" 8 | "layeh.com/gumble/gumble/MumbleProto" 9 | ) 10 | 11 | // BanList is a list of server ban entries. 12 | // 13 | // Whenever a ban is changed, it does not come into effect until the ban list 14 | // is sent back to the server. 15 | type BanList []*Ban 16 | 17 | // Add creates a new ban list entry with the given parameters. 18 | func (b *BanList) Add(address net.IP, mask net.IPMask, reason string, duration time.Duration) *Ban { 19 | ban := &Ban{ 20 | Address: address, 21 | Mask: mask, 22 | Reason: reason, 23 | Duration: duration, 24 | } 25 | *b = append(*b, ban) 26 | return ban 27 | } 28 | 29 | // Ban represents an entry in the server ban list. 30 | // 31 | // This type should not be initialized manually. Instead, create new ban 32 | // entries using BanList.Add(). 33 | type Ban struct { 34 | // The banned IP address. 35 | Address net.IP 36 | // The IP mask that the ban applies to. 37 | Mask net.IPMask 38 | // The name of the banned user. 39 | Name string 40 | // The certificate hash of the banned user. 41 | Hash string 42 | // The reason for the ban. 43 | Reason string 44 | // The start time from which the ban applies. 45 | Start time.Time 46 | // How long the ban is for. 47 | Duration time.Duration 48 | 49 | unban bool 50 | } 51 | 52 | // SetAddress sets the banned IP address. 53 | func (b *Ban) SetAddress(address net.IP) { 54 | b.Address = address 55 | } 56 | 57 | // SetMask sets the IP mask that the ban applies to. 58 | func (b *Ban) SetMask(mask net.IPMask) { 59 | b.Mask = mask 60 | } 61 | 62 | // SetReason changes the reason for the ban. 63 | func (b *Ban) SetReason(reason string) { 64 | b.Reason = reason 65 | } 66 | 67 | // SetDuration changes the duration of the ban. 68 | func (b *Ban) SetDuration(duration time.Duration) { 69 | b.Duration = duration 70 | } 71 | 72 | // Unban will unban the user from the server. 73 | func (b *Ban) Unban() { 74 | b.unban = true 75 | } 76 | 77 | // Ban will ban the user from the server. This is only useful if Unban() was 78 | // called on the ban entry. 79 | func (b *Ban) Ban() { 80 | b.unban = false 81 | } 82 | 83 | func (b BanList) writeMessage(client *Client) error { 84 | packet := MumbleProto.BanList{ 85 | Query: proto.Bool(false), 86 | } 87 | 88 | for _, ban := range b { 89 | if !ban.unban { 90 | maskSize, _ := ban.Mask.Size() 91 | packet.Bans = append(packet.Bans, &MumbleProto.BanList_BanEntry{ 92 | Address: ban.Address, 93 | Mask: proto.Uint32(uint32(maskSize)), 94 | Reason: &ban.Reason, 95 | Duration: proto.Uint32(uint32(ban.Duration / time.Second)), 96 | }) 97 | } 98 | } 99 | 100 | return client.Conn.WriteProto(&packet) 101 | } 102 | -------------------------------------------------------------------------------- /gumble/ping.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "net" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // PingResponse contains information about a server that responded to a UDP 14 | // ping packet. 15 | type PingResponse struct { 16 | // The address of the pinged server. 17 | Address *net.UDPAddr 18 | // The round-trip time from the client to the server. 19 | Ping time.Duration 20 | // The server's version. Only the Version field and SemanticVersion method of 21 | // the value will be valid. 22 | Version Version 23 | // The number users currently connected to the server. 24 | ConnectedUsers int 25 | // The maximum number of users that can connect to the server. 26 | MaximumUsers int 27 | // The maximum audio bitrate per user for the server. 28 | MaximumBitrate int 29 | } 30 | 31 | // Ping sends a UDP ping packet to the given server. If interval is positive, 32 | // the packet is retransmitted at every interval. 33 | // 34 | // Returns a PingResponse and nil on success. The function will return nil and 35 | // an error if a valid response is not received after the given timeout. 36 | func Ping(address string, interval, timeout time.Duration) (*PingResponse, error) { 37 | if timeout < 0 { 38 | return nil, errors.New("gumble: timeout must be positive") 39 | } 40 | deadline := time.Now().Add(timeout) 41 | conn, err := net.DialTimeout("udp", address, timeout) 42 | if err != nil { 43 | return nil, err 44 | } 45 | defer conn.Close() 46 | conn.SetReadDeadline(deadline) 47 | 48 | var ( 49 | idsLock sync.Mutex 50 | ids = make(map[string]time.Time) 51 | ) 52 | 53 | buildSendPacket := func() { 54 | var packet [12]byte 55 | if _, err := rand.Read(packet[4:]); err != nil { 56 | return 57 | } 58 | id := string(packet[4:]) 59 | idsLock.Lock() 60 | ids[id] = time.Now() 61 | idsLock.Unlock() 62 | conn.Write(packet[:]) 63 | } 64 | 65 | if interval > 0 { 66 | end := make(chan struct{}) 67 | defer close(end) 68 | go func() { 69 | ticker := time.NewTicker(interval) 70 | defer ticker.Stop() 71 | for { 72 | select { 73 | case <-ticker.C: 74 | buildSendPacket() 75 | case <-end: 76 | return 77 | } 78 | } 79 | }() 80 | } 81 | 82 | buildSendPacket() 83 | 84 | for { 85 | var incoming [24]byte 86 | if _, err := io.ReadFull(conn, incoming[:]); err != nil { 87 | return nil, err 88 | } 89 | id := string(incoming[4:12]) 90 | idsLock.Lock() 91 | sendTime, ok := ids[id] 92 | idsLock.Unlock() 93 | if !ok { 94 | continue 95 | } 96 | 97 | return &PingResponse{ 98 | Address: conn.RemoteAddr().(*net.UDPAddr), 99 | Ping: time.Since(sendTime), 100 | Version: Version{ 101 | Version: binary.BigEndian.Uint32(incoming[0:]), 102 | }, 103 | ConnectedUsers: int(binary.BigEndian.Uint32(incoming[12:])), 104 | MaximumUsers: int(binary.BigEndian.Uint32(incoming[16:])), 105 | MaximumBitrate: int(binary.BigEndian.Uint32(incoming[20:])), 106 | }, nil 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gumbleutil/listener.go: -------------------------------------------------------------------------------- 1 | package gumbleutil 2 | 3 | import ( 4 | "layeh.com/gumble/gumble" 5 | ) 6 | 7 | // Listener is a struct that implements the gumble.EventListener interface. The 8 | // corresponding event function in the struct is called if it is non-nil. 9 | type Listener struct { 10 | Connect func(e *gumble.ConnectEvent) 11 | Disconnect func(e *gumble.DisconnectEvent) 12 | TextMessage func(e *gumble.TextMessageEvent) 13 | UserChange func(e *gumble.UserChangeEvent) 14 | ChannelChange func(e *gumble.ChannelChangeEvent) 15 | PermissionDenied func(e *gumble.PermissionDeniedEvent) 16 | UserList func(e *gumble.UserListEvent) 17 | ACL func(e *gumble.ACLEvent) 18 | BanList func(e *gumble.BanListEvent) 19 | ContextActionChange func(e *gumble.ContextActionChangeEvent) 20 | ServerConfig func(e *gumble.ServerConfigEvent) 21 | } 22 | 23 | var _ gumble.EventListener = (*Listener)(nil) 24 | 25 | // OnConnect implements gumble.EventListener.OnConnect. 26 | func (l Listener) OnConnect(e *gumble.ConnectEvent) { 27 | if l.Connect != nil { 28 | l.Connect(e) 29 | } 30 | } 31 | 32 | // OnDisconnect implements gumble.EventListener.OnDisconnect. 33 | func (l Listener) OnDisconnect(e *gumble.DisconnectEvent) { 34 | if l.Disconnect != nil { 35 | l.Disconnect(e) 36 | } 37 | } 38 | 39 | // OnTextMessage implements gumble.EventListener.OnTextMessage. 40 | func (l Listener) OnTextMessage(e *gumble.TextMessageEvent) { 41 | if l.TextMessage != nil { 42 | l.TextMessage(e) 43 | } 44 | } 45 | 46 | // OnUserChange implements gumble.EventListener.OnUserChange. 47 | func (l Listener) OnUserChange(e *gumble.UserChangeEvent) { 48 | if l.UserChange != nil { 49 | l.UserChange(e) 50 | } 51 | } 52 | 53 | // OnChannelChange implements gumble.EventListener.OnChannelChange. 54 | func (l Listener) OnChannelChange(e *gumble.ChannelChangeEvent) { 55 | if l.ChannelChange != nil { 56 | l.ChannelChange(e) 57 | } 58 | } 59 | 60 | // OnPermissionDenied implements gumble.EventListener.OnPermissionDenied. 61 | func (l Listener) OnPermissionDenied(e *gumble.PermissionDeniedEvent) { 62 | if l.PermissionDenied != nil { 63 | l.PermissionDenied(e) 64 | } 65 | } 66 | 67 | // OnUserList implements gumble.EventListener.OnUserList. 68 | func (l Listener) OnUserList(e *gumble.UserListEvent) { 69 | if l.UserList != nil { 70 | l.UserList(e) 71 | } 72 | } 73 | 74 | // OnACL implements gumble.EventListener.OnACL. 75 | func (l Listener) OnACL(e *gumble.ACLEvent) { 76 | if l.ACL != nil { 77 | l.ACL(e) 78 | } 79 | } 80 | 81 | // OnBanList implements gumble.EventListener.OnBanList. 82 | func (l Listener) OnBanList(e *gumble.BanListEvent) { 83 | if l.BanList != nil { 84 | l.BanList(e) 85 | } 86 | } 87 | 88 | // OnContextActionChange implements gumble.EventListener.OnContextActionChange. 89 | func (l Listener) OnContextActionChange(e *gumble.ContextActionChangeEvent) { 90 | if l.ContextActionChange != nil { 91 | l.ContextActionChange(e) 92 | } 93 | } 94 | 95 | // OnServerConfig implements gumble.EventListener.OnServerConfig. 96 | func (l Listener) OnServerConfig(e *gumble.ServerConfigEvent) { 97 | if l.ServerConfig != nil { 98 | l.ServerConfig(e) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /gumble/acl.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "github.com/golang/protobuf/proto" 5 | "layeh.com/gumble/gumble/MumbleProto" 6 | ) 7 | 8 | // ACL contains a list of ACLGroups and ACLRules linked to a channel. 9 | type ACL struct { 10 | // The channel to which the ACL belongs. 11 | Channel *Channel 12 | // The ACL's groups. 13 | Groups []*ACLGroup 14 | // The ACL's rules. 15 | Rules []*ACLRule 16 | // Does the ACL inherits the parent channel's ACLs? 17 | Inherits bool 18 | } 19 | 20 | func (a *ACL) writeMessage(client *Client) error { 21 | packet := MumbleProto.ACL{ 22 | ChannelId: &a.Channel.ID, 23 | Groups: make([]*MumbleProto.ACL_ChanGroup, len(a.Groups)), 24 | Acls: make([]*MumbleProto.ACL_ChanACL, len(a.Rules)), 25 | InheritAcls: &a.Inherits, 26 | Query: proto.Bool(false), 27 | } 28 | 29 | for i, group := range a.Groups { 30 | packet.Groups[i] = &MumbleProto.ACL_ChanGroup{ 31 | Name: &group.Name, 32 | Inherit: &group.InheritUsers, 33 | Inheritable: &group.Inheritable, 34 | Add: make([]uint32, 0, len(group.UsersAdd)), 35 | Remove: make([]uint32, 0, len(group.UsersRemove)), 36 | } 37 | for _, user := range group.UsersAdd { 38 | packet.Groups[i].Add = append(packet.Groups[i].Add, user.UserID) 39 | } 40 | for _, user := range group.UsersRemove { 41 | packet.Groups[i].Remove = append(packet.Groups[i].Remove, user.UserID) 42 | } 43 | } 44 | 45 | for i, rule := range a.Rules { 46 | packet.Acls[i] = &MumbleProto.ACL_ChanACL{ 47 | ApplyHere: &rule.AppliesCurrent, 48 | ApplySubs: &rule.AppliesChildren, 49 | Grant: proto.Uint32(uint32(rule.Granted)), 50 | Deny: proto.Uint32(uint32(rule.Denied)), 51 | } 52 | if rule.User != nil { 53 | packet.Acls[i].UserId = &rule.User.UserID 54 | } 55 | if rule.Group != nil { 56 | packet.Acls[i].Group = &rule.Group.Name 57 | } 58 | } 59 | 60 | return client.Conn.WriteProto(&packet) 61 | } 62 | 63 | // ACLUser is a registered user who is part of or can be part of an ACL group 64 | // or rule. 65 | type ACLUser struct { 66 | // The user ID of the user. 67 | UserID uint32 68 | // The name of the user. 69 | Name string 70 | } 71 | 72 | // ACLGroup is a named group of registered users which can be used in an 73 | // ACLRule. 74 | type ACLGroup struct { 75 | // The ACL group name. 76 | Name string 77 | // Is the group inherited from the parent channel's ACL? 78 | Inherited bool 79 | // Are group members are inherited from the parent channel's ACL? 80 | InheritUsers bool 81 | // Can the group be inherited by child channels? 82 | Inheritable bool 83 | // The users who are explicitly added to, explicitly removed from, and 84 | // inherited into the group. 85 | UsersAdd, UsersRemove, UsersInherited map[uint32]*ACLUser 86 | } 87 | 88 | // ACL group names that are built-in. 89 | const ( 90 | ACLGroupEveryone = "all" 91 | ACLGroupAuthenticated = "auth" 92 | ACLGroupInsideChannel = "in" 93 | ACLGroupOutsideChannel = "out" 94 | ) 95 | 96 | // ACLRule is a set of granted and denied permissions given to an ACLUser or 97 | // ACLGroup. 98 | type ACLRule struct { 99 | // Does the rule apply to the channel in which the rule is defined? 100 | AppliesCurrent bool 101 | // Does the rule apply to the children of the channel in which the rule is 102 | // defined? 103 | AppliesChildren bool 104 | // Is the rule inherited from the parent channel's ACL? 105 | Inherited bool 106 | 107 | // The permissions granted by the rule. 108 | Granted Permission 109 | // The permissions denied by the rule. 110 | Denied Permission 111 | 112 | // The ACL user the rule applies to. Can be nil. 113 | User *ACLUser 114 | // The ACL group the rule applies to. Can be nil. 115 | Group *ACLGroup 116 | } 117 | -------------------------------------------------------------------------------- /gumbleopenal/stream.go: -------------------------------------------------------------------------------- 1 | package gumbleopenal 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "time" 7 | 8 | "github.com/dchote/go-openal/openal" 9 | "layeh.com/gumble/gumble" 10 | ) 11 | 12 | var ( 13 | ErrState = errors.New("gumbleopenal: invalid state") 14 | ) 15 | 16 | type Stream struct { 17 | client *gumble.Client 18 | link gumble.Detacher 19 | 20 | deviceSource *openal.CaptureDevice 21 | sourceFrameSize int 22 | sourceStop chan bool 23 | 24 | deviceSink *openal.Device 25 | contextSink *openal.Context 26 | } 27 | 28 | func New(client *gumble.Client) (*Stream, error) { 29 | s := &Stream{ 30 | client: client, 31 | sourceFrameSize: client.Config.AudioFrameSize(), 32 | } 33 | 34 | s.deviceSource = openal.CaptureOpenDevice("", gumble.AudioSampleRate, openal.FormatMono16, uint32(s.sourceFrameSize)) 35 | 36 | s.deviceSink = openal.OpenDevice("") 37 | s.contextSink = s.deviceSink.CreateContext() 38 | s.contextSink.Activate() 39 | 40 | s.link = client.Config.AttachAudio(s) 41 | 42 | return s, nil 43 | } 44 | 45 | func (s *Stream) Destroy() { 46 | s.link.Detach() 47 | if s.deviceSource != nil { 48 | s.StopSource() 49 | s.deviceSource.CaptureCloseDevice() 50 | s.deviceSource = nil 51 | } 52 | if s.deviceSink != nil { 53 | s.contextSink.Destroy() 54 | s.deviceSink.CloseDevice() 55 | s.contextSink = nil 56 | s.deviceSink = nil 57 | } 58 | } 59 | 60 | func (s *Stream) StartSource() error { 61 | if s.sourceStop != nil { 62 | return ErrState 63 | } 64 | s.deviceSource.CaptureStart() 65 | s.sourceStop = make(chan bool) 66 | go s.sourceRoutine() 67 | return nil 68 | } 69 | 70 | func (s *Stream) StopSource() error { 71 | if s.sourceStop == nil { 72 | return ErrState 73 | } 74 | close(s.sourceStop) 75 | s.sourceStop = nil 76 | s.deviceSource.CaptureStop() 77 | return nil 78 | } 79 | 80 | func (s *Stream) OnAudioStream(e *gumble.AudioStreamEvent) { 81 | go func() { 82 | source := openal.NewSource() 83 | emptyBufs := openal.NewBuffers(8) 84 | reclaim := func() { 85 | if n := source.BuffersProcessed(); n > 0 { 86 | reclaimedBufs := make(openal.Buffers, n) 87 | source.UnqueueBuffers(reclaimedBufs) 88 | emptyBufs = append(emptyBufs, reclaimedBufs...) 89 | } 90 | } 91 | var raw [gumble.AudioMaximumFrameSize * 2]byte 92 | for packet := range e.C { 93 | samples := len(packet.AudioBuffer) 94 | if samples > cap(raw) { 95 | continue 96 | } 97 | for i, value := range packet.AudioBuffer { 98 | binary.LittleEndian.PutUint16(raw[i*2:], uint16(value)) 99 | } 100 | reclaim() 101 | if len(emptyBufs) == 0 { 102 | continue 103 | } 104 | last := len(emptyBufs) - 1 105 | buffer := emptyBufs[last] 106 | emptyBufs = emptyBufs[:last] 107 | buffer.SetData(openal.FormatMono16, raw[:samples*2], gumble.AudioSampleRate) 108 | source.QueueBuffer(buffer) 109 | if source.State() != openal.Playing { 110 | source.Play() 111 | } 112 | } 113 | reclaim() 114 | emptyBufs.Delete() 115 | source.Delete() 116 | }() 117 | } 118 | 119 | func (s *Stream) sourceRoutine() { 120 | interval := s.client.Config.AudioInterval 121 | frameSize := s.client.Config.AudioFrameSize() 122 | 123 | if frameSize != s.sourceFrameSize { 124 | s.deviceSource.CaptureCloseDevice() 125 | s.sourceFrameSize = frameSize 126 | s.deviceSource = openal.CaptureOpenDevice("", gumble.AudioSampleRate, openal.FormatMono16, uint32(s.sourceFrameSize)) 127 | } 128 | 129 | ticker := time.NewTicker(interval) 130 | defer ticker.Stop() 131 | 132 | stop := s.sourceStop 133 | 134 | outgoing := s.client.AudioOutgoing() 135 | defer close(outgoing) 136 | 137 | for { 138 | select { 139 | case <-stop: 140 | return 141 | case <-ticker.C: 142 | buff := s.deviceSource.CaptureSamples(uint32(frameSize)) 143 | if len(buff) != frameSize*2 { 144 | continue 145 | } 146 | int16Buffer := make([]int16, frameSize) 147 | for i := range int16Buffer { 148 | int16Buffer[i] = int16(binary.LittleEndian.Uint16(buff[i*2 : (i+1)*2])) 149 | } 150 | outgoing <- gumble.AudioBuffer(int16Buffer) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /gumble/listeners.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | type eventItem struct { 4 | parent *Listeners 5 | prev, next *eventItem 6 | listener EventListener 7 | } 8 | 9 | func (e *eventItem) Detach() { 10 | if e.prev == nil { 11 | e.parent.head = e.next 12 | } else { 13 | e.prev.next = e.next 14 | } 15 | if e.next == nil { 16 | e.parent.tail = e.prev 17 | } else { 18 | e.next.prev = e.prev 19 | } 20 | } 21 | 22 | // Listeners is a list of event listeners. Each attached listener is called in 23 | // sequence when a Client event is triggered. 24 | type Listeners struct { 25 | head, tail *eventItem 26 | } 27 | 28 | // Attach adds a new event listener to the end of the current list of listeners. 29 | func (e *Listeners) Attach(listener EventListener) Detacher { 30 | item := &eventItem{ 31 | parent: e, 32 | prev: e.tail, 33 | listener: listener, 34 | } 35 | if e.head == nil { 36 | e.head = item 37 | } 38 | if e.tail != nil { 39 | e.tail.next = item 40 | } 41 | e.tail = item 42 | return item 43 | } 44 | 45 | func (e *Listeners) onConnect(event *ConnectEvent) { 46 | event.Client.volatile.Lock() 47 | for item := e.head; item != nil; item = item.next { 48 | event.Client.volatile.Unlock() 49 | item.listener.OnConnect(event) 50 | event.Client.volatile.Lock() 51 | } 52 | event.Client.volatile.Unlock() 53 | } 54 | 55 | func (e *Listeners) onDisconnect(event *DisconnectEvent) { 56 | event.Client.volatile.Lock() 57 | for item := e.head; item != nil; item = item.next { 58 | event.Client.volatile.Unlock() 59 | item.listener.OnDisconnect(event) 60 | event.Client.volatile.Lock() 61 | } 62 | event.Client.volatile.Unlock() 63 | } 64 | 65 | func (e *Listeners) onTextMessage(event *TextMessageEvent) { 66 | event.Client.volatile.Lock() 67 | for item := e.head; item != nil; item = item.next { 68 | event.Client.volatile.Unlock() 69 | item.listener.OnTextMessage(event) 70 | event.Client.volatile.Lock() 71 | } 72 | event.Client.volatile.Unlock() 73 | } 74 | 75 | func (e *Listeners) onUserChange(event *UserChangeEvent) { 76 | event.Client.volatile.Lock() 77 | for item := e.head; item != nil; item = item.next { 78 | event.Client.volatile.Unlock() 79 | item.listener.OnUserChange(event) 80 | event.Client.volatile.Lock() 81 | } 82 | event.Client.volatile.Unlock() 83 | } 84 | 85 | func (e *Listeners) onChannelChange(event *ChannelChangeEvent) { 86 | event.Client.volatile.Lock() 87 | for item := e.head; item != nil; item = item.next { 88 | event.Client.volatile.Unlock() 89 | item.listener.OnChannelChange(event) 90 | event.Client.volatile.Lock() 91 | } 92 | event.Client.volatile.Unlock() 93 | } 94 | 95 | func (e *Listeners) onPermissionDenied(event *PermissionDeniedEvent) { 96 | event.Client.volatile.Lock() 97 | for item := e.head; item != nil; item = item.next { 98 | event.Client.volatile.Unlock() 99 | item.listener.OnPermissionDenied(event) 100 | event.Client.volatile.Lock() 101 | } 102 | event.Client.volatile.Unlock() 103 | } 104 | 105 | func (e *Listeners) onUserList(event *UserListEvent) { 106 | event.Client.volatile.Lock() 107 | for item := e.head; item != nil; item = item.next { 108 | event.Client.volatile.Unlock() 109 | item.listener.OnUserList(event) 110 | event.Client.volatile.Lock() 111 | } 112 | event.Client.volatile.Unlock() 113 | } 114 | 115 | func (e *Listeners) onACL(event *ACLEvent) { 116 | event.Client.volatile.Lock() 117 | for item := e.head; item != nil; item = item.next { 118 | event.Client.volatile.Unlock() 119 | item.listener.OnACL(event) 120 | event.Client.volatile.Lock() 121 | } 122 | event.Client.volatile.Unlock() 123 | } 124 | 125 | func (e *Listeners) onBanList(event *BanListEvent) { 126 | event.Client.volatile.Lock() 127 | for item := e.head; item != nil; item = item.next { 128 | event.Client.volatile.Unlock() 129 | item.listener.OnBanList(event) 130 | event.Client.volatile.Lock() 131 | } 132 | event.Client.volatile.Unlock() 133 | } 134 | 135 | func (e *Listeners) onContextActionChange(event *ContextActionChangeEvent) { 136 | event.Client.volatile.Lock() 137 | for item := e.head; item != nil; item = item.next { 138 | event.Client.volatile.Unlock() 139 | item.listener.OnContextActionChange(event) 140 | event.Client.volatile.Lock() 141 | } 142 | event.Client.volatile.Unlock() 143 | } 144 | 145 | func (e *Listeners) onServerConfig(event *ServerConfigEvent) { 146 | event.Client.volatile.Lock() 147 | for item := e.head; item != nil; item = item.next { 148 | event.Client.volatile.Unlock() 149 | item.listener.OnServerConfig(event) 150 | event.Client.volatile.Lock() 151 | } 152 | event.Client.volatile.Unlock() 153 | } 154 | -------------------------------------------------------------------------------- /gumbleffmpeg/stream.go: -------------------------------------------------------------------------------- 1 | package gumbleffmpeg 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "os/exec" 8 | "strconv" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | "layeh.com/gumble/gumble" 14 | ) 15 | 16 | // State represents the state of a Stream. 17 | type State int32 18 | 19 | // Valid states of Stream. 20 | const ( 21 | StateInitial State = iota + 1 22 | StatePlaying 23 | StatePaused 24 | StateStopped 25 | ) 26 | 27 | // Stream is an audio stream that encodes media through ffmpeg and sends it to 28 | // the server. 29 | // 30 | // A stream can only be used once; it cannot be started after it is stopped. 31 | type Stream struct { 32 | // Command to execute to play the file. Defaults to "ffmpeg". 33 | Command string 34 | // Playback volume (can be changed while the source is playing). 35 | Volume float32 36 | // Audio source (cannot be changed after stream starts). 37 | Source Source 38 | // Starting offset. 39 | Offset time.Duration 40 | 41 | client *gumble.Client 42 | cmd *exec.Cmd 43 | pipe io.ReadCloser 44 | pause chan struct{} 45 | elapsed int64 46 | 47 | state State 48 | 49 | l sync.Mutex 50 | wg sync.WaitGroup 51 | } 52 | 53 | // New returns a new Stream for the given gumble Client and Source. 54 | func New(client *gumble.Client, source Source) *Stream { 55 | return &Stream{ 56 | client: client, 57 | Volume: 1.0, 58 | Source: source, 59 | Command: "ffmpeg", 60 | pause: make(chan struct{}), 61 | state: StateInitial, 62 | } 63 | } 64 | 65 | // Play begins playing 66 | func (s *Stream) Play() error { 67 | s.l.Lock() 68 | defer s.l.Unlock() 69 | 70 | switch s.state { 71 | case StatePaused: 72 | s.state = StatePlaying 73 | go s.process() 74 | return nil 75 | case StatePlaying: 76 | return errors.New("gumbleffmpeg: stream already playing") 77 | case StateStopped: 78 | return errors.New("gumbleffmpeg: stream has stopped") 79 | } 80 | 81 | // fresh stream 82 | if s.Source == nil { 83 | return errors.New("gumbleffmpeg: nil source") 84 | } 85 | 86 | args := s.Source.arguments() 87 | if s.Offset > 0 { 88 | args = append([]string{"-ss", strconv.FormatFloat(s.Offset.Seconds(), 'f', -1, 64)}, args...) 89 | } 90 | args = append(args, "-ac", strconv.Itoa(gumble.AudioChannels), "-ar", strconv.Itoa(gumble.AudioSampleRate), "-f", "s16le", "-") 91 | cmd := exec.Command(s.Command, args...) 92 | var err error 93 | s.pipe, err = cmd.StdoutPipe() 94 | if err != nil { 95 | return err 96 | } 97 | if err := s.Source.start(cmd); err != nil { 98 | return err 99 | } 100 | if err := cmd.Start(); err != nil { 101 | s.Source.done() 102 | return err 103 | } 104 | s.wg.Add(1) 105 | s.cmd = cmd 106 | s.state = StatePlaying 107 | go s.process() 108 | return nil 109 | } 110 | 111 | // State returns the state of the stream. 112 | func (s *Stream) State() State { 113 | s.l.Lock() 114 | defer s.l.Unlock() 115 | return s.state 116 | } 117 | 118 | // Pause pauses a playing stream. 119 | func (s *Stream) Pause() error { 120 | s.l.Lock() 121 | if s.state != StatePlaying { 122 | s.l.Unlock() 123 | return errors.New("gumbleffmpeg: stream is not playing") 124 | } 125 | s.state = StatePaused 126 | s.l.Unlock() 127 | s.pause <- struct{}{} 128 | return nil 129 | } 130 | 131 | // Stop stops the stream. 132 | func (s *Stream) Stop() error { 133 | s.l.Lock() 134 | switch s.state { 135 | case StateStopped, StateInitial: 136 | s.l.Unlock() 137 | return errors.New("gumbleffmpeg: stream is not playing nor paused") 138 | } 139 | s.cleanup() 140 | s.Wait() 141 | return nil 142 | } 143 | 144 | // Wait returns once the stream has stopped playing. 145 | func (s *Stream) Wait() { 146 | s.wg.Wait() 147 | } 148 | 149 | // Elapsed returns the amount of audio that has been played by the stream. 150 | func (s *Stream) Elapsed() time.Duration { 151 | return time.Duration(atomic.LoadInt64(&s.elapsed)) 152 | } 153 | 154 | func (s *Stream) process() { 155 | // s.state has been set to StatePlaying 156 | 157 | interval := s.client.Config.AudioInterval 158 | frameSize := s.client.Config.AudioFrameSize() 159 | 160 | byteBuffer := make([]byte, frameSize*2) 161 | 162 | outgoing := s.client.AudioOutgoing() 163 | defer close(outgoing) 164 | 165 | ticker := time.NewTicker(interval) 166 | defer ticker.Stop() 167 | 168 | for { 169 | select { 170 | case <-s.pause: 171 | return 172 | case <-ticker.C: 173 | if _, err := io.ReadFull(s.pipe, byteBuffer); err != nil { 174 | s.l.Lock() 175 | s.cleanup() 176 | return 177 | } 178 | int16Buffer := make([]int16, frameSize) 179 | for i := range int16Buffer { 180 | float := float32(int16(binary.LittleEndian.Uint16(byteBuffer[i*2 : (i+1)*2]))) 181 | int16Buffer[i] = int16(s.Volume * float) 182 | } 183 | atomic.AddInt64(&s.elapsed, int64(interval)) 184 | outgoing <- gumble.AudioBuffer(int16Buffer) 185 | } 186 | } 187 | } 188 | 189 | func (s *Stream) cleanup() { 190 | defer s.l.Unlock() 191 | // s.l has been acquired 192 | if s.state == StateStopped { 193 | return 194 | } 195 | s.cmd.Process.Kill() 196 | s.cmd.Wait() 197 | s.Source.done() 198 | for len(s.pause) > 0 { 199 | <-s.pause 200 | } 201 | s.state = StateStopped 202 | s.wg.Done() 203 | } 204 | -------------------------------------------------------------------------------- /gumble/conn.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/golang/protobuf/proto" 12 | "layeh.com/gumble/gumble/MumbleProto" 13 | "layeh.com/gumble/gumble/varint" 14 | ) 15 | 16 | // DefaultPort is the default port on which Mumble servers listen. 17 | const DefaultPort = 64738 18 | 19 | // Conn represents a control protocol connection to a Mumble client/server. 20 | type Conn struct { 21 | sync.Mutex 22 | net.Conn 23 | 24 | MaximumPacketBytes int 25 | Timeout time.Duration 26 | 27 | buffer []byte 28 | } 29 | 30 | // NewConn creates a new Conn with the given net.Conn. 31 | func NewConn(conn net.Conn) *Conn { 32 | return &Conn{ 33 | Conn: conn, 34 | Timeout: time.Second * 20, 35 | MaximumPacketBytes: 1024 * 1024 * 10, 36 | } 37 | } 38 | 39 | // ReadPacket reads a packet from the server. Returns the packet type, the 40 | // packet data, and nil on success. 41 | // 42 | // This function should only be called by a single go routine. 43 | func (c *Conn) ReadPacket() (uint16, []byte, error) { 44 | c.Conn.SetReadDeadline(time.Now().Add(c.Timeout)) 45 | var header [6]byte 46 | if _, err := io.ReadFull(c.Conn, header[:]); err != nil { 47 | return 0, nil, err 48 | } 49 | pType := binary.BigEndian.Uint16(header[:]) 50 | pLength := binary.BigEndian.Uint32(header[2:]) 51 | pLengthInt := int(pLength) 52 | if pLengthInt > c.MaximumPacketBytes { 53 | return 0, nil, errors.New("gumble: packet larger than maximum allowed size") 54 | } 55 | if pLengthInt > len(c.buffer) { 56 | c.buffer = make([]byte, pLengthInt) 57 | } 58 | if _, err := io.ReadFull(c.Conn, c.buffer[:pLengthInt]); err != nil { 59 | return 0, nil, err 60 | } 61 | return pType, c.buffer[:pLengthInt], nil 62 | } 63 | 64 | // WriteAudio writes an audio packet to the connection. 65 | func (c *Conn) WriteAudio(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) error { 66 | var buff [1 + varint.MaxVarintLen*2]byte 67 | buff[0] = (format << 5) | target 68 | n := varint.Encode(buff[1:], sequence) 69 | if n == 0 { 70 | return errors.New("gumble: varint out of range") 71 | } 72 | l := int64(len(data)) 73 | if final { 74 | l |= 0x2000 75 | } 76 | m := varint.Encode(buff[1+n:], l) 77 | if m == 0 { 78 | return errors.New("gumble: varint out of range") 79 | } 80 | header := buff[:1+n+m] 81 | 82 | var positionalLength int 83 | if X != nil { 84 | positionalLength = 3 * 4 85 | } 86 | 87 | c.Lock() 88 | defer c.Unlock() 89 | 90 | if err := c.writeHeader(1, uint32(len(header)+len(data)+positionalLength)); err != nil { 91 | return err 92 | } 93 | if _, err := c.Conn.Write(header); err != nil { 94 | return err 95 | } 96 | if _, err := c.Conn.Write(data); err != nil { 97 | return err 98 | } 99 | 100 | if positionalLength > 0 { 101 | if err := binary.Write(c.Conn, binary.LittleEndian, *X); err != nil { 102 | return err 103 | } 104 | if err := binary.Write(c.Conn, binary.LittleEndian, *Y); err != nil { 105 | return err 106 | } 107 | if err := binary.Write(c.Conn, binary.LittleEndian, *Z); err != nil { 108 | return err 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | 115 | // WritePacket writes a data packet of the given type to the connection. 116 | func (c *Conn) WritePacket(ptype uint16, data []byte) error { 117 | c.Lock() 118 | defer c.Unlock() 119 | if err := c.writeHeader(uint16(ptype), uint32(len(data))); err != nil { 120 | return err 121 | } 122 | if _, err := c.Conn.Write(data); err != nil { 123 | return err 124 | } 125 | return nil 126 | } 127 | 128 | func (c *Conn) writeHeader(pType uint16, pLength uint32) error { 129 | var header [6]byte 130 | binary.BigEndian.PutUint16(header[:], pType) 131 | binary.BigEndian.PutUint32(header[2:], pLength) 132 | if _, err := c.Conn.Write(header[:]); err != nil { 133 | return err 134 | } 135 | return nil 136 | } 137 | 138 | // WriteProto writes a protocol buffer message to the connection. 139 | func (c *Conn) WriteProto(message proto.Message) error { 140 | var protoType uint16 141 | switch message.(type) { 142 | case *MumbleProto.Version: 143 | protoType = 0 144 | case *MumbleProto.Authenticate: 145 | protoType = 2 146 | case *MumbleProto.Ping: 147 | protoType = 3 148 | case *MumbleProto.Reject: 149 | protoType = 4 150 | case *MumbleProto.ServerSync: 151 | protoType = 5 152 | case *MumbleProto.ChannelRemove: 153 | protoType = 6 154 | case *MumbleProto.ChannelState: 155 | protoType = 7 156 | case *MumbleProto.UserRemove: 157 | protoType = 8 158 | case *MumbleProto.UserState: 159 | protoType = 9 160 | case *MumbleProto.BanList: 161 | protoType = 10 162 | case *MumbleProto.TextMessage: 163 | protoType = 11 164 | case *MumbleProto.PermissionDenied: 165 | protoType = 12 166 | case *MumbleProto.ACL: 167 | protoType = 13 168 | case *MumbleProto.QueryUsers: 169 | protoType = 14 170 | case *MumbleProto.CryptSetup: 171 | protoType = 15 172 | case *MumbleProto.ContextActionModify: 173 | protoType = 16 174 | case *MumbleProto.ContextAction: 175 | protoType = 17 176 | case *MumbleProto.UserList: 177 | protoType = 18 178 | case *MumbleProto.VoiceTarget: 179 | protoType = 19 180 | case *MumbleProto.PermissionQuery: 181 | protoType = 20 182 | case *MumbleProto.CodecVersion: 183 | protoType = 21 184 | case *MumbleProto.UserStats: 185 | protoType = 22 186 | case *MumbleProto.RequestBlob: 187 | protoType = 23 188 | case *MumbleProto.ServerConfig: 189 | protoType = 24 190 | case *MumbleProto.SuggestConfig: 191 | protoType = 25 192 | default: 193 | return errors.New("gumble: unknown message type") 194 | } 195 | data, err := proto.Marshal(message) 196 | if err != nil { 197 | return err 198 | } 199 | return c.WritePacket(protoType, data) 200 | } 201 | -------------------------------------------------------------------------------- /gumble/channel.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "github.com/golang/protobuf/proto" 5 | "layeh.com/gumble/gumble/MumbleProto" 6 | ) 7 | 8 | // Channel represents a channel in the server's channel tree. 9 | type Channel struct { 10 | // The channel's unique ID. 11 | ID uint32 12 | // The channel's name. 13 | Name string 14 | // The channel's parent. nil if the channel is the root channel. 15 | Parent *Channel 16 | // The channels directly underneath the channel. 17 | Children Channels 18 | // The channels that are linked to the channel. 19 | Links Channels 20 | // The users currently in the channel. 21 | Users Users 22 | // The channel's description. Contains the empty string if the channel does 23 | // not have a description, or if it needs to be requested. 24 | Description string 25 | // The channel's description hash. nil if Channel.Description has 26 | // been populated. 27 | DescriptionHash []byte 28 | // The maximum number of users allowed in the channel. If the value is zero, 29 | // the maximum number of users per-channel is dictated by the server's 30 | // "usersperchannel" setting. 31 | MaxUsers uint32 32 | // The position at which the channel should be displayed in an ordered list. 33 | Position int32 34 | // Is the channel temporary? 35 | Temporary bool 36 | 37 | client *Client 38 | } 39 | 40 | // IsRoot returns true if the channel is the server's root channel. 41 | func (c *Channel) IsRoot() bool { 42 | return c.ID == 0 43 | } 44 | 45 | // Add will add a sub-channel to the given channel. 46 | func (c *Channel) Add(name string, temporary bool) { 47 | packet := MumbleProto.ChannelState{ 48 | Parent: &c.ID, 49 | Name: &name, 50 | Temporary: &temporary, 51 | } 52 | c.client.Conn.WriteProto(&packet) 53 | } 54 | 55 | // Remove will remove the given channel and all sub-channels from the server's 56 | // channel tree. 57 | func (c *Channel) Remove() { 58 | packet := MumbleProto.ChannelRemove{ 59 | ChannelId: &c.ID, 60 | } 61 | c.client.Conn.WriteProto(&packet) 62 | } 63 | 64 | // SetName will set the name of the channel. This will have no effect if the 65 | // channel is the server's root channel. 66 | func (c *Channel) SetName(name string) { 67 | packet := MumbleProto.ChannelState{ 68 | ChannelId: &c.ID, 69 | Name: &name, 70 | } 71 | c.client.Conn.WriteProto(&packet) 72 | } 73 | 74 | // SetDescription will set the description of the channel. 75 | func (c *Channel) SetDescription(description string) { 76 | packet := MumbleProto.ChannelState{ 77 | ChannelId: &c.ID, 78 | Description: &description, 79 | } 80 | c.client.Conn.WriteProto(&packet) 81 | } 82 | 83 | // SetPosition will set the position of the channel. 84 | func (c *Channel) SetPosition(position int32) { 85 | packet := MumbleProto.ChannelState{ 86 | ChannelId: &c.ID, 87 | Position: &position, 88 | } 89 | c.client.Conn.WriteProto(&packet) 90 | } 91 | 92 | // SetMaxUsers will set the maximum number of users allowed in the channel. 93 | func (c *Channel) SetMaxUsers(maxUsers uint32) { 94 | packet := MumbleProto.ChannelState{ 95 | ChannelId: &c.ID, 96 | MaxUsers: &maxUsers, 97 | } 98 | c.client.Conn.WriteProto(&packet) 99 | } 100 | 101 | // Find returns a channel whose path (by channel name) from the current channel 102 | // is equal to the arguments passed. 103 | // 104 | // For example, given the following server channel tree: 105 | // Root 106 | // Child 1 107 | // Child 2 108 | // Child 2.1 109 | // Child 2.2 110 | // Child 2.2.1 111 | // Child 3 112 | // To get the "Child 2.2.1" channel: 113 | // root.Find("Child 2", "Child 2.2", "Child 2.2.1") 114 | func (c *Channel) Find(names ...string) *Channel { 115 | if len(names) == 0 { 116 | return c 117 | } 118 | for _, child := range c.Children { 119 | if child.Name == names[0] { 120 | return child.Find(names[1:]...) 121 | } 122 | } 123 | return nil 124 | } 125 | 126 | // RequestDescription requests that the actual channel description 127 | // (i.e. non-hashed) be sent to the client. 128 | func (c *Channel) RequestDescription() { 129 | packet := MumbleProto.RequestBlob{ 130 | ChannelDescription: []uint32{c.ID}, 131 | } 132 | c.client.Conn.WriteProto(&packet) 133 | } 134 | 135 | // RequestACL requests that the channel's ACL to be sent to the client. 136 | func (c *Channel) RequestACL() { 137 | packet := MumbleProto.ACL{ 138 | ChannelId: &c.ID, 139 | Query: proto.Bool(true), 140 | } 141 | c.client.Conn.WriteProto(&packet) 142 | } 143 | 144 | // RequestPermission requests that the channel's permission information to be 145 | // sent to the client. 146 | // 147 | // Note: the server will not reply to the request if the client has up-to-date 148 | // permission information. 149 | func (c *Channel) RequestPermission() { 150 | packet := MumbleProto.PermissionQuery{ 151 | ChannelId: &c.ID, 152 | } 153 | c.client.Conn.WriteProto(&packet) 154 | } 155 | 156 | // Send will send a text message to the channel. 157 | func (c *Channel) Send(message string, recursive bool) { 158 | textMessage := TextMessage{ 159 | Message: message, 160 | } 161 | if recursive { 162 | textMessage.Trees = []*Channel{c} 163 | } else { 164 | textMessage.Channels = []*Channel{c} 165 | } 166 | c.client.Send(&textMessage) 167 | } 168 | 169 | // Permission returns the permissions the user has in the channel, or nil if 170 | // the permissions are unknown. 171 | func (c *Channel) Permission() *Permission { 172 | return c.client.permissions[c.ID] 173 | } 174 | 175 | // Link links the given channels to the channel. 176 | func (c *Channel) Link(channel ...*Channel) { 177 | packet := MumbleProto.ChannelState{ 178 | ChannelId: &c.ID, 179 | LinksAdd: make([]uint32, len(channel)), 180 | } 181 | for i, ch := range channel { 182 | packet.LinksAdd[i] = ch.ID 183 | } 184 | c.client.Conn.WriteProto(&packet) 185 | } 186 | 187 | // Unlink unlinks the given channels from the channel. If no arguments are 188 | // passed, all linked channels are unlinked. 189 | func (c *Channel) Unlink(channel ...*Channel) { 190 | packet := MumbleProto.ChannelState{ 191 | ChannelId: &c.ID, 192 | } 193 | if len(channel) == 0 { 194 | packet.LinksRemove = make([]uint32, len(c.Links)) 195 | i := 0 196 | for channelID := range c.Links { 197 | packet.LinksRemove[i] = channelID 198 | i++ 199 | } 200 | } else { 201 | packet.LinksRemove = make([]uint32, len(channel)) 202 | for i, ch := range channel { 203 | packet.LinksRemove[i] = ch.ID 204 | } 205 | } 206 | c.client.Conn.WriteProto(&packet) 207 | } 208 | -------------------------------------------------------------------------------- /gumble/user.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "github.com/golang/protobuf/proto" 5 | "layeh.com/gumble/gumble/MumbleProto" 6 | ) 7 | 8 | // User represents a user that is currently connected to the server. 9 | type User struct { 10 | // The user's unique session ID. 11 | Session uint32 12 | // The user's ID. Contains an invalid value if the user is not registered. 13 | UserID uint32 14 | // The user's name. 15 | Name string 16 | // The channel that the user is currently in. 17 | Channel *Channel 18 | 19 | // Has the user has been muted? 20 | Muted bool 21 | // Has the user been deafened? 22 | Deafened bool 23 | // Has the user been suppressed? 24 | Suppressed bool 25 | // Has the user been muted by him/herself? 26 | SelfMuted bool 27 | // Has the user been deafened by him/herself? 28 | SelfDeafened bool 29 | // Is the user a priority speaker in the channel? 30 | PrioritySpeaker bool 31 | // Is the user recording audio? 32 | Recording bool 33 | 34 | // The user's comment. Contains the empty string if the user does not have a 35 | // comment, or if the comment needs to be requested. 36 | Comment string 37 | // The user's comment hash. nil if User.Comment has been populated. 38 | CommentHash []byte 39 | // The hash of the user's certificate (can be empty). 40 | Hash string 41 | // The user's texture (avatar). nil if the user does not have a 42 | // texture, or if the texture needs to be requested. 43 | Texture []byte 44 | // The user's texture hash. nil if User.Texture has been populated. 45 | TextureHash []byte 46 | 47 | // The user's stats. Contains nil if the stats have not yet been requested. 48 | Stats *UserStats 49 | 50 | client *Client 51 | decoder AudioDecoder 52 | } 53 | 54 | // SetTexture sets the user's texture. 55 | func (u *User) SetTexture(texture []byte) { 56 | packet := MumbleProto.UserState{ 57 | Session: &u.Session, 58 | Texture: texture, 59 | } 60 | u.client.Conn.WriteProto(&packet) 61 | } 62 | 63 | // SetPrioritySpeaker sets if the user is a priority speaker in the channel. 64 | func (u *User) SetPrioritySpeaker(prioritySpeaker bool) { 65 | packet := MumbleProto.UserState{ 66 | Session: &u.Session, 67 | PrioritySpeaker: &prioritySpeaker, 68 | } 69 | u.client.Conn.WriteProto(&packet) 70 | } 71 | 72 | // SetRecording sets if the user is recording audio. 73 | func (u *User) SetRecording(recording bool) { 74 | packet := MumbleProto.UserState{ 75 | Session: &u.Session, 76 | Recording: &recording, 77 | } 78 | u.client.Conn.WriteProto(&packet) 79 | } 80 | 81 | // IsRegistered returns true if the user's certificate has been registered with 82 | // the server. A registered user will have a valid user ID. 83 | func (u *User) IsRegistered() bool { 84 | return u.UserID > 0 85 | } 86 | 87 | // Register will register the user with the server. If the client has 88 | // permission to do so, the user will shortly be given a UserID. 89 | func (u *User) Register() { 90 | packet := MumbleProto.UserState{ 91 | Session: &u.Session, 92 | UserId: proto.Uint32(0), 93 | } 94 | u.client.Conn.WriteProto(&packet) 95 | } 96 | 97 | // SetComment will set the user's comment to the given string. The user's 98 | // comment will be erased if the comment is set to the empty string. 99 | func (u *User) SetComment(comment string) { 100 | packet := MumbleProto.UserState{ 101 | Session: &u.Session, 102 | Comment: &comment, 103 | } 104 | u.client.Conn.WriteProto(&packet) 105 | } 106 | 107 | // Move will move the user to the given channel. 108 | func (u *User) Move(channel *Channel) { 109 | packet := MumbleProto.UserState{ 110 | Session: &u.Session, 111 | ChannelId: &channel.ID, 112 | } 113 | u.client.Conn.WriteProto(&packet) 114 | } 115 | 116 | // Kick will kick the user from the server. 117 | func (u *User) Kick(reason string) { 118 | packet := MumbleProto.UserRemove{ 119 | Session: &u.Session, 120 | Reason: &reason, 121 | } 122 | u.client.Conn.WriteProto(&packet) 123 | } 124 | 125 | // Ban will ban the user from the server. 126 | func (u *User) Ban(reason string) { 127 | packet := MumbleProto.UserRemove{ 128 | Session: &u.Session, 129 | Reason: &reason, 130 | Ban: proto.Bool(true), 131 | } 132 | u.client.Conn.WriteProto(&packet) 133 | } 134 | 135 | // SetMuted sets whether the user can transmit audio or not. 136 | func (u *User) SetMuted(muted bool) { 137 | packet := MumbleProto.UserState{ 138 | Session: &u.Session, 139 | Mute: &muted, 140 | } 141 | u.client.Conn.WriteProto(&packet) 142 | } 143 | 144 | // SetSuppressed sets whether the user is suppressed by the server or not. 145 | func (u *User) SetSuppressed(supressed bool) { 146 | packet := MumbleProto.UserState{ 147 | Session: &u.Session, 148 | Suppress: &supressed, 149 | } 150 | u.client.Conn.WriteProto(&packet) 151 | } 152 | 153 | // SetDeafened sets whether the user can receive audio or not. 154 | func (u *User) SetDeafened(muted bool) { 155 | packet := MumbleProto.UserState{ 156 | Session: &u.Session, 157 | Deaf: &muted, 158 | } 159 | u.client.Conn.WriteProto(&packet) 160 | } 161 | 162 | // SetSelfMuted sets whether the user can transmit audio or not. 163 | // 164 | // This method should only be called on Client.Self(). 165 | func (u *User) SetSelfMuted(muted bool) { 166 | packet := MumbleProto.UserState{ 167 | Session: &u.Session, 168 | SelfMute: &muted, 169 | } 170 | u.client.Conn.WriteProto(&packet) 171 | } 172 | 173 | // SetSelfDeafened sets whether the user can receive audio or not. 174 | // 175 | // This method should only be called on Client.Self(). 176 | func (u *User) SetSelfDeafened(muted bool) { 177 | packet := MumbleProto.UserState{ 178 | Session: &u.Session, 179 | SelfDeaf: &muted, 180 | } 181 | u.client.Conn.WriteProto(&packet) 182 | } 183 | 184 | // RequestStats requests that the user's stats be sent to the client. 185 | func (u *User) RequestStats() { 186 | packet := MumbleProto.UserStats{ 187 | Session: &u.Session, 188 | } 189 | u.client.Conn.WriteProto(&packet) 190 | } 191 | 192 | // RequestTexture requests that the user's actual texture (i.e. non-hashed) be 193 | // sent to the client. 194 | func (u *User) RequestTexture() { 195 | packet := MumbleProto.RequestBlob{ 196 | SessionTexture: []uint32{u.Session}, 197 | } 198 | u.client.Conn.WriteProto(&packet) 199 | } 200 | 201 | // RequestComment requests that the user's actual comment (i.e. non-hashed) be 202 | // sent to the client. 203 | func (u *User) RequestComment() { 204 | packet := MumbleProto.RequestBlob{ 205 | SessionComment: []uint32{u.Session}, 206 | } 207 | u.client.Conn.WriteProto(&packet) 208 | } 209 | 210 | // Send will send a text message to the user. 211 | func (u *User) Send(message string) { 212 | textMessage := TextMessage{ 213 | Users: []*User{u}, 214 | Message: message, 215 | } 216 | u.client.Send(&textMessage) 217 | } 218 | 219 | // SetPlugin sets the user's plugin data. 220 | // 221 | // Plugins are currently only used for positional audio. Clients will receive 222 | // positional audio information from other users if their plugin context is the 223 | // same. The official Mumble client sets the context to: 224 | // 225 | // PluginShortName + "\x00" + AdditionalContextInformation 226 | func (u *User) SetPlugin(context []byte, identity string) { 227 | packet := MumbleProto.UserState{ 228 | Session: &u.Session, 229 | PluginContext: context, 230 | PluginIdentity: &identity, 231 | } 232 | u.client.Conn.WriteProto(&packet) 233 | } 234 | -------------------------------------------------------------------------------- /gumble/event.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "layeh.com/gumble/gumble/MumbleProto" 5 | ) 6 | 7 | // EventListener is the interface that must be implemented by a type if it 8 | // wishes to be notified of Client events. 9 | // 10 | // Listener methods are executed synchronously as event happen. They also block 11 | // network reads from happening until all handlers for an event are called. 12 | // Therefore, it is not recommended to do any long processing from inside of 13 | // these methods. 14 | type EventListener interface { 15 | OnConnect(e *ConnectEvent) 16 | OnDisconnect(e *DisconnectEvent) 17 | OnTextMessage(e *TextMessageEvent) 18 | OnUserChange(e *UserChangeEvent) 19 | OnChannelChange(e *ChannelChangeEvent) 20 | OnPermissionDenied(e *PermissionDeniedEvent) 21 | OnUserList(e *UserListEvent) 22 | OnACL(e *ACLEvent) 23 | OnBanList(e *BanListEvent) 24 | OnContextActionChange(e *ContextActionChangeEvent) 25 | OnServerConfig(e *ServerConfigEvent) 26 | } 27 | 28 | // ConnectEvent is the event that is passed to EventListener.OnConnect. 29 | type ConnectEvent struct { 30 | Client *Client 31 | WelcomeMessage *string 32 | MaximumBitrate *int 33 | } 34 | 35 | // DisconnectType specifies why a Client disconnected from a server. 36 | type DisconnectType int 37 | 38 | // Client disconnect reasons. 39 | const ( 40 | DisconnectError DisconnectType = iota + 1 41 | DisconnectKicked 42 | DisconnectBanned 43 | DisconnectUser 44 | ) 45 | 46 | // Has returns true if the DisconnectType has changeType part of its bitmask. 47 | func (d DisconnectType) Has(changeType DisconnectType) bool { 48 | return d&changeType == changeType 49 | } 50 | 51 | // DisconnectEvent is the event that is passed to EventListener.OnDisconnect. 52 | type DisconnectEvent struct { 53 | Client *Client 54 | Type DisconnectType 55 | 56 | String string 57 | } 58 | 59 | // TextMessageEvent is the event that is passed to EventListener.OnTextMessage. 60 | type TextMessageEvent struct { 61 | Client *Client 62 | TextMessage 63 | } 64 | 65 | // UserChangeType is a bitmask of items that changed for a user. 66 | type UserChangeType int 67 | 68 | // User change items. 69 | const ( 70 | UserChangeConnected UserChangeType = 1 << iota 71 | UserChangeDisconnected 72 | UserChangeKicked 73 | UserChangeBanned 74 | UserChangeRegistered 75 | UserChangeUnregistered 76 | UserChangeName 77 | UserChangeChannel 78 | UserChangeComment 79 | UserChangeAudio 80 | UserChangeTexture 81 | UserChangePrioritySpeaker 82 | UserChangeRecording 83 | UserChangeStats 84 | ) 85 | 86 | // Has returns true if the UserChangeType has changeType part of its bitmask. 87 | func (u UserChangeType) Has(changeType UserChangeType) bool { 88 | return u&changeType == changeType 89 | } 90 | 91 | // UserChangeEvent is the event that is passed to EventListener.OnUserChange. 92 | type UserChangeEvent struct { 93 | Client *Client 94 | Type UserChangeType 95 | User *User 96 | Actor *User 97 | 98 | String string 99 | } 100 | 101 | // ChannelChangeType is a bitmask of items that changed for a channel. 102 | type ChannelChangeType int 103 | 104 | // Channel change items. 105 | const ( 106 | ChannelChangeCreated ChannelChangeType = 1 << iota 107 | ChannelChangeRemoved 108 | ChannelChangeMoved 109 | ChannelChangeName 110 | ChannelChangeLinks 111 | ChannelChangeDescription 112 | ChannelChangePosition 113 | ChannelChangePermission 114 | ChannelChangeMaxUsers 115 | ) 116 | 117 | // Has returns true if the ChannelChangeType has changeType part of its 118 | // bitmask. 119 | func (c ChannelChangeType) Has(changeType ChannelChangeType) bool { 120 | return c&changeType == changeType 121 | } 122 | 123 | // ChannelChangeEvent is the event that is passed to 124 | // EventListener.OnChannelChange. 125 | type ChannelChangeEvent struct { 126 | Client *Client 127 | Type ChannelChangeType 128 | Channel *Channel 129 | } 130 | 131 | // PermissionDeniedType specifies why a Client was denied permission to perform 132 | // a particular action. 133 | type PermissionDeniedType int 134 | 135 | // Permission denied types. 136 | const ( 137 | PermissionDeniedOther PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_Text) 138 | PermissionDeniedPermission PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_Permission) 139 | PermissionDeniedSuperUser PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_SuperUser) 140 | PermissionDeniedInvalidChannelName PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_ChannelName) 141 | PermissionDeniedTextTooLong PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_TextTooLong) 142 | PermissionDeniedTemporaryChannel PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_TemporaryChannel) 143 | PermissionDeniedMissingCertificate PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_MissingCertificate) 144 | PermissionDeniedInvalidUserName PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_UserName) 145 | PermissionDeniedChannelFull PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_ChannelFull) 146 | PermissionDeniedNestingLimit PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_NestingLimit) 147 | PermissionDeniedChannelCountLimit PermissionDeniedType = PermissionDeniedType(MumbleProto.PermissionDenied_ChannelCountLimit) 148 | ) 149 | 150 | // Has returns true if the PermissionDeniedType has changeType part of its 151 | // bitmask. 152 | func (p PermissionDeniedType) Has(changeType PermissionDeniedType) bool { 153 | return p&changeType == changeType 154 | } 155 | 156 | // PermissionDeniedEvent is the event that is passed to 157 | // EventListener.OnPermissionDenied. 158 | type PermissionDeniedEvent struct { 159 | Client *Client 160 | Type PermissionDeniedType 161 | Channel *Channel 162 | User *User 163 | 164 | Permission Permission 165 | String string 166 | } 167 | 168 | // UserListEvent is the event that is passed to EventListener.OnUserList. 169 | type UserListEvent struct { 170 | Client *Client 171 | UserList RegisteredUsers 172 | } 173 | 174 | // ACLEvent is the event that is passed to EventListener.OnACL. 175 | type ACLEvent struct { 176 | Client *Client 177 | ACL *ACL 178 | } 179 | 180 | // BanListEvent is the event that is passed to EventListener.OnBanList. 181 | type BanListEvent struct { 182 | Client *Client 183 | BanList BanList 184 | } 185 | 186 | // ContextActionChangeType specifies how a ContextAction changed. 187 | type ContextActionChangeType int 188 | 189 | // ContextAction change types. 190 | const ( 191 | ContextActionAdd ContextActionChangeType = ContextActionChangeType(MumbleProto.ContextActionModify_Add) 192 | ContextActionRemove ContextActionChangeType = ContextActionChangeType(MumbleProto.ContextActionModify_Remove) 193 | ) 194 | 195 | // ContextActionChangeEvent is the event that is passed to 196 | // EventListener.OnContextActionChange. 197 | type ContextActionChangeEvent struct { 198 | Client *Client 199 | Type ContextActionChangeType 200 | ContextAction *ContextAction 201 | } 202 | 203 | // ServerConfigEvent is the event that is passed to 204 | // EventListener.OnServerConfig. 205 | type ServerConfigEvent struct { 206 | Client *Client 207 | 208 | MaximumBitrate *int 209 | WelcomeMessage *string 210 | AllowHTML *bool 211 | MaximumMessageLength *int 212 | MaximumImageMessageLength *int 213 | MaximumUsers *int 214 | 215 | CodecAlpha *int32 216 | CodecBeta *int32 217 | CodecPreferAlpha *bool 218 | CodecOpus *bool 219 | 220 | SuggestVersion *Version 221 | SuggestPositional *bool 222 | SuggestPushToTalk *bool 223 | } 224 | -------------------------------------------------------------------------------- /misc/gumble.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /gumble/client.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 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. 17 | type State int 18 | 19 | const ( 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. 34 | const ClientVersion = 1<<16 | 3<<8 | 0 35 | 36 | // Client is the type used to create a connection to a server. 37 | type 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). 81 | func 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. 94 | func 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. 170 | func (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). 178 | func (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. 196 | func (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. 227 | func (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. 253 | func (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. 259 | func (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. 267 | func (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. 279 | func (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. 287 | func (c *Client) Send(message Message) { 288 | message.writeMessage(c) 289 | } 290 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /gumble/handlers.go: -------------------------------------------------------------------------------- 1 | package gumble 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/binary" 6 | "errors" 7 | "math" 8 | "net" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/golang/protobuf/proto" 13 | "layeh.com/gumble/gumble/MumbleProto" 14 | "layeh.com/gumble/gumble/varint" 15 | ) 16 | 17 | var ( 18 | errUnimplementedHandler = errors.New("gumble: the handler has not been implemented") 19 | errIncompleteProtobuf = errors.New("gumble: protobuf message is missing a required field") 20 | errInvalidProtobuf = errors.New("gumble: protobuf message has an invalid field") 21 | errUnsupportedAudio = errors.New("gumble: unsupported audio codec") 22 | errNoCodec = errors.New("gumble: no audio codec") 23 | ) 24 | 25 | var handlers = [...]func(*Client, []byte) error{ 26 | (*Client).handleVersion, 27 | (*Client).handleUDPTunnel, 28 | (*Client).handleAuthenticate, 29 | (*Client).handlePing, 30 | (*Client).handleReject, 31 | (*Client).handleServerSync, 32 | (*Client).handleChannelRemove, 33 | (*Client).handleChannelState, 34 | (*Client).handleUserRemove, 35 | (*Client).handleUserState, 36 | (*Client).handleBanList, 37 | (*Client).handleTextMessage, 38 | (*Client).handlePermissionDenied, 39 | (*Client).handleACL, 40 | (*Client).handleQueryUsers, 41 | (*Client).handleCryptSetup, 42 | (*Client).handleContextActionModify, 43 | (*Client).handleContextAction, 44 | (*Client).handleUserList, 45 | (*Client).handleVoiceTarget, 46 | (*Client).handlePermissionQuery, 47 | (*Client).handleCodecVersion, 48 | (*Client).handleUserStats, 49 | (*Client).handleRequestBlob, 50 | (*Client).handleServerConfig, 51 | (*Client).handleSuggestConfig, 52 | } 53 | 54 | func parseVersion(packet *MumbleProto.Version) Version { 55 | var version Version 56 | if packet.Version != nil { 57 | version.Version = *packet.Version 58 | } 59 | if packet.Release != nil { 60 | version.Release = *packet.Release 61 | } 62 | if packet.Os != nil { 63 | version.OS = *packet.Os 64 | } 65 | if packet.OsVersion != nil { 66 | version.OSVersion = *packet.OsVersion 67 | } 68 | return version 69 | } 70 | 71 | func (c *Client) handleVersion(buffer []byte) error { 72 | var packet MumbleProto.Version 73 | if err := proto.Unmarshal(buffer, &packet); err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | func (c *Client) handleUDPTunnel(buffer []byte) error { 80 | if len(buffer) < 1 { 81 | return errInvalidProtobuf 82 | } 83 | audioType := (buffer[0] >> 5) & 0x7 84 | audioTarget := buffer[0] & 0x1F 85 | 86 | // Opus only 87 | // TODO: add handling for other packet types 88 | if audioType != audioCodecIDOpus { 89 | return errUnsupportedAudio 90 | } 91 | 92 | // Session 93 | buffer = buffer[1:] 94 | session, n := varint.Decode(buffer) 95 | if n <= 0 { 96 | return errInvalidProtobuf 97 | } 98 | buffer = buffer[n:] 99 | user := c.Users[uint32(session)] 100 | if user == nil { 101 | return errInvalidProtobuf 102 | } 103 | decoder := user.decoder 104 | if decoder == nil { 105 | // TODO: decoder pool 106 | // TODO: de-reference after stream is done 107 | codec := c.audioCodec 108 | if codec == nil { 109 | return errNoCodec 110 | } 111 | decoder = codec.NewDecoder() 112 | user.decoder = decoder 113 | } 114 | 115 | // Sequence 116 | // TODO: use in jitter buffer 117 | _, n = varint.Decode(buffer) 118 | if n <= 0 { 119 | return errInvalidProtobuf 120 | } 121 | buffer = buffer[n:] 122 | 123 | // Length 124 | length, n := varint.Decode(buffer) 125 | if n <= 0 { 126 | return errInvalidProtobuf 127 | } 128 | buffer = buffer[n:] 129 | // Opus audio packets set the 13th bit in the size field as the terminator. 130 | audioLength := int(length) &^ 0x2000 131 | if audioLength > len(buffer) { 132 | return errInvalidProtobuf 133 | } 134 | 135 | pcm, err := decoder.Decode(buffer[:audioLength], AudioMaximumFrameSize) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | event := AudioPacket{ 141 | Client: c, 142 | Sender: user, 143 | Target: &VoiceTarget{ 144 | ID: uint32(audioTarget), 145 | }, 146 | AudioBuffer: AudioBuffer(pcm), 147 | } 148 | 149 | if len(buffer)-audioLength == 3*4 { 150 | // the packet has positional audio data; 3x float32 151 | buffer = buffer[audioLength:] 152 | 153 | event.X = math.Float32frombits(binary.LittleEndian.Uint32(buffer)) 154 | event.Y = math.Float32frombits(binary.LittleEndian.Uint32(buffer[4:])) 155 | event.Z = math.Float32frombits(binary.LittleEndian.Uint32(buffer[8:])) 156 | event.HasPosition = true 157 | } 158 | 159 | c.volatile.Lock() 160 | for item := c.Config.AudioListeners.head; item != nil; item = item.next { 161 | c.volatile.Unlock() 162 | ch := item.streams[user] 163 | if ch == nil { 164 | ch = make(chan *AudioPacket) 165 | item.streams[user] = ch 166 | event := AudioStreamEvent{ 167 | Client: c, 168 | User: user, 169 | C: ch, 170 | } 171 | item.listener.OnAudioStream(&event) 172 | } 173 | ch <- &event 174 | c.volatile.Lock() 175 | } 176 | c.volatile.Unlock() 177 | 178 | return nil 179 | } 180 | 181 | func (c *Client) handleAuthenticate(buffer []byte) error { 182 | return errUnimplementedHandler 183 | } 184 | 185 | func (c *Client) handlePing(buffer []byte) error { 186 | var packet MumbleProto.Ping 187 | if err := proto.Unmarshal(buffer, &packet); err != nil { 188 | return err 189 | } 190 | 191 | atomic.AddUint32(&c.tcpPacketsReceived, 1) 192 | 193 | if packet.Timestamp != nil { 194 | diff := time.Since(time.Unix(0, int64(*packet.Timestamp))) 195 | 196 | index := int(c.tcpPacketsReceived) - 1 197 | if index >= len(c.tcpPingTimes) { 198 | for i := 1; i < len(c.tcpPingTimes); i++ { 199 | c.tcpPingTimes[i-1] = c.tcpPingTimes[i] 200 | } 201 | index = len(c.tcpPingTimes) - 1 202 | } 203 | 204 | // average is in milliseconds 205 | ping := float32(diff.Seconds() * 1000) 206 | c.tcpPingTimes[index] = ping 207 | 208 | var sum float32 209 | for i := 0; i <= index; i++ { 210 | sum += c.tcpPingTimes[i] 211 | } 212 | avg := sum / float32(index+1) 213 | 214 | sum = 0 215 | for i := 0; i <= index; i++ { 216 | sum += (avg - c.tcpPingTimes[i]) * (avg - c.tcpPingTimes[i]) 217 | } 218 | variance := sum / float32(index+1) 219 | 220 | atomic.StoreUint32(&c.tcpPingAvg, math.Float32bits(avg)) 221 | atomic.StoreUint32(&c.tcpPingVar, math.Float32bits(variance)) 222 | } 223 | return nil 224 | } 225 | 226 | func (c *Client) handleReject(buffer []byte) error { 227 | var packet MumbleProto.Reject 228 | if err := proto.Unmarshal(buffer, &packet); err != nil { 229 | return err 230 | } 231 | 232 | if c.State() != StateConnected { 233 | return errInvalidProtobuf 234 | } 235 | 236 | err := &RejectError{} 237 | 238 | if packet.Type != nil { 239 | err.Type = RejectType(*packet.Type) 240 | } 241 | if packet.Reason != nil { 242 | err.Reason = *packet.Reason 243 | } 244 | c.connect <- err 245 | c.Conn.Close() 246 | return nil 247 | } 248 | 249 | func (c *Client) handleServerSync(buffer []byte) error { 250 | var packet MumbleProto.ServerSync 251 | if err := proto.Unmarshal(buffer, &packet); err != nil { 252 | return err 253 | } 254 | event := ConnectEvent{ 255 | Client: c, 256 | } 257 | 258 | if packet.Session != nil { 259 | { 260 | c.volatile.Lock() 261 | 262 | c.Self = c.Users[*packet.Session] 263 | 264 | c.volatile.Unlock() 265 | } 266 | } 267 | if packet.WelcomeText != nil { 268 | event.WelcomeMessage = packet.WelcomeText 269 | } 270 | if packet.MaxBandwidth != nil { 271 | val := int(*packet.MaxBandwidth) 272 | event.MaximumBitrate = &val 273 | } 274 | atomic.StoreUint32(&c.state, uint32(StateSynced)) 275 | c.Config.Listeners.onConnect(&event) 276 | close(c.connect) 277 | return nil 278 | } 279 | 280 | func (c *Client) handleChannelRemove(buffer []byte) error { 281 | var packet MumbleProto.ChannelRemove 282 | if err := proto.Unmarshal(buffer, &packet); err != nil { 283 | return err 284 | } 285 | 286 | if packet.ChannelId == nil { 287 | return errIncompleteProtobuf 288 | } 289 | 290 | var channel *Channel 291 | { 292 | c.volatile.Lock() 293 | 294 | channelID := *packet.ChannelId 295 | channel = c.Channels[channelID] 296 | if channel == nil { 297 | c.volatile.Unlock() 298 | return errInvalidProtobuf 299 | } 300 | channel.client = nil 301 | delete(c.Channels, channelID) 302 | delete(c.permissions, channelID) 303 | if parent := channel.Parent; parent != nil { 304 | delete(parent.Children, channel.ID) 305 | } 306 | for _, link := range channel.Links { 307 | delete(link.Links, channelID) 308 | } 309 | 310 | c.volatile.Unlock() 311 | } 312 | 313 | if c.State() == StateSynced { 314 | event := ChannelChangeEvent{ 315 | Client: c, 316 | Type: ChannelChangeRemoved, 317 | Channel: channel, 318 | } 319 | c.Config.Listeners.onChannelChange(&event) 320 | } 321 | return nil 322 | } 323 | 324 | func (c *Client) handleChannelState(buffer []byte) error { 325 | var packet MumbleProto.ChannelState 326 | if err := proto.Unmarshal(buffer, &packet); err != nil { 327 | return err 328 | } 329 | 330 | if packet.ChannelId == nil { 331 | return errIncompleteProtobuf 332 | } 333 | event := ChannelChangeEvent{ 334 | Client: c, 335 | } 336 | 337 | { 338 | c.volatile.Lock() 339 | 340 | channelID := *packet.ChannelId 341 | channel := c.Channels[channelID] 342 | if channel == nil { 343 | channel = c.Channels.create(channelID) 344 | channel.client = c 345 | 346 | event.Type |= ChannelChangeCreated 347 | } 348 | event.Channel = channel 349 | if packet.Parent != nil { 350 | if channel.Parent != nil { 351 | delete(channel.Parent.Children, channelID) 352 | } 353 | newParent := c.Channels[*packet.Parent] 354 | if newParent != channel.Parent { 355 | event.Type |= ChannelChangeMoved 356 | } 357 | channel.Parent = newParent 358 | if channel.Parent != nil { 359 | channel.Parent.Children[channel.ID] = channel 360 | } 361 | } 362 | if packet.Name != nil { 363 | if *packet.Name != channel.Name { 364 | event.Type |= ChannelChangeName 365 | } 366 | channel.Name = *packet.Name 367 | } 368 | if packet.Links != nil { 369 | channel.Links = make(Channels) 370 | event.Type |= ChannelChangeLinks 371 | for _, channelID := range packet.Links { 372 | if c := c.Channels[channelID]; c != nil { 373 | channel.Links[channelID] = c 374 | } 375 | } 376 | } 377 | for _, channelID := range packet.LinksAdd { 378 | if c := c.Channels[channelID]; c != nil { 379 | event.Type |= ChannelChangeLinks 380 | channel.Links[channelID] = c 381 | c.Links[channel.ID] = channel 382 | } 383 | } 384 | for _, channelID := range packet.LinksRemove { 385 | if c := c.Channels[channelID]; c != nil { 386 | event.Type |= ChannelChangeLinks 387 | delete(channel.Links, channelID) 388 | delete(c.Links, channel.ID) 389 | } 390 | } 391 | if packet.Description != nil { 392 | if *packet.Description != channel.Description { 393 | event.Type |= ChannelChangeDescription 394 | } 395 | channel.Description = *packet.Description 396 | channel.DescriptionHash = nil 397 | } 398 | if packet.Temporary != nil { 399 | channel.Temporary = *packet.Temporary 400 | } 401 | if packet.Position != nil { 402 | if *packet.Position != channel.Position { 403 | event.Type |= ChannelChangePosition 404 | } 405 | channel.Position = *packet.Position 406 | } 407 | if packet.DescriptionHash != nil { 408 | event.Type |= ChannelChangeDescription 409 | channel.DescriptionHash = packet.DescriptionHash 410 | channel.Description = "" 411 | } 412 | if packet.MaxUsers != nil { 413 | event.Type |= ChannelChangeMaxUsers 414 | channel.MaxUsers = *packet.MaxUsers 415 | } 416 | 417 | c.volatile.Unlock() 418 | } 419 | 420 | if c.State() == StateSynced { 421 | c.Config.Listeners.onChannelChange(&event) 422 | } 423 | return nil 424 | } 425 | 426 | func (c *Client) handleUserRemove(buffer []byte) error { 427 | var packet MumbleProto.UserRemove 428 | if err := proto.Unmarshal(buffer, &packet); err != nil { 429 | return err 430 | } 431 | 432 | if packet.Session == nil { 433 | return errIncompleteProtobuf 434 | } 435 | event := UserChangeEvent{ 436 | Client: c, 437 | Type: UserChangeDisconnected, 438 | } 439 | 440 | { 441 | c.volatile.Lock() 442 | 443 | session := *packet.Session 444 | event.User = c.Users[session] 445 | if event.User == nil { 446 | c.volatile.Unlock() 447 | return errInvalidProtobuf 448 | } 449 | if packet.Actor != nil { 450 | event.Actor = c.Users[*packet.Actor] 451 | if event.Actor == nil { 452 | c.volatile.Unlock() 453 | return errInvalidProtobuf 454 | } 455 | event.Type |= UserChangeKicked 456 | } 457 | 458 | event.User.client = nil 459 | if event.User.Channel != nil { 460 | delete(event.User.Channel.Users, session) 461 | } 462 | delete(c.Users, session) 463 | if packet.Reason != nil { 464 | event.String = *packet.Reason 465 | } 466 | if packet.Ban != nil && *packet.Ban { 467 | event.Type |= UserChangeBanned 468 | } 469 | if event.User == c.Self { 470 | if packet.Ban != nil && *packet.Ban { 471 | c.disconnectEvent.Type = DisconnectBanned 472 | } else { 473 | c.disconnectEvent.Type = DisconnectKicked 474 | } 475 | } 476 | 477 | c.volatile.Unlock() 478 | } 479 | 480 | if c.State() == StateSynced { 481 | c.Config.Listeners.onUserChange(&event) 482 | } 483 | return nil 484 | } 485 | 486 | func (c *Client) handleUserState(buffer []byte) error { 487 | var packet MumbleProto.UserState 488 | if err := proto.Unmarshal(buffer, &packet); err != nil { 489 | return err 490 | } 491 | 492 | if packet.Session == nil { 493 | return errIncompleteProtobuf 494 | } 495 | event := UserChangeEvent{ 496 | Client: c, 497 | } 498 | var user, actor *User 499 | { 500 | c.volatile.Lock() 501 | 502 | session := *packet.Session 503 | user = c.Users[session] 504 | if user == nil { 505 | user = c.Users.create(session) 506 | user.Channel = c.Channels[0] 507 | user.client = c 508 | 509 | event.Type |= UserChangeConnected 510 | 511 | if user.Channel == nil { 512 | c.volatile.Unlock() 513 | return errInvalidProtobuf 514 | } 515 | event.Type |= UserChangeChannel 516 | user.Channel.Users[session] = user 517 | } 518 | 519 | event.User = user 520 | if packet.Actor != nil { 521 | actor = c.Users[*packet.Actor] 522 | if actor == nil { 523 | c.volatile.Unlock() 524 | return errInvalidProtobuf 525 | } 526 | event.Actor = actor 527 | } 528 | if packet.Name != nil { 529 | if *packet.Name != user.Name { 530 | event.Type |= UserChangeName 531 | } 532 | user.Name = *packet.Name 533 | } 534 | if packet.UserId != nil { 535 | if *packet.UserId != user.UserID && !event.Type.Has(UserChangeConnected) { 536 | if *packet.UserId != math.MaxUint32 { 537 | event.Type |= UserChangeRegistered 538 | user.UserID = *packet.UserId 539 | } else { 540 | event.Type |= UserChangeUnregistered 541 | user.UserID = 0 542 | } 543 | } else { 544 | user.UserID = *packet.UserId 545 | } 546 | } 547 | if packet.ChannelId != nil { 548 | if user.Channel != nil { 549 | delete(user.Channel.Users, user.Session) 550 | } 551 | newChannel := c.Channels[*packet.ChannelId] 552 | if newChannel == nil { 553 | c.volatile.Lock() 554 | return errInvalidProtobuf 555 | } 556 | if newChannel != user.Channel { 557 | event.Type |= UserChangeChannel 558 | user.Channel = newChannel 559 | } 560 | user.Channel.Users[user.Session] = user 561 | } 562 | if packet.Mute != nil { 563 | if *packet.Mute != user.Muted { 564 | event.Type |= UserChangeAudio 565 | } 566 | user.Muted = *packet.Mute 567 | } 568 | if packet.Deaf != nil { 569 | if *packet.Deaf != user.Deafened { 570 | event.Type |= UserChangeAudio 571 | } 572 | user.Deafened = *packet.Deaf 573 | } 574 | if packet.Suppress != nil { 575 | if *packet.Suppress != user.Suppressed { 576 | event.Type |= UserChangeAudio 577 | } 578 | user.Suppressed = *packet.Suppress 579 | } 580 | if packet.SelfMute != nil { 581 | if *packet.SelfMute != user.SelfMuted { 582 | event.Type |= UserChangeAudio 583 | } 584 | user.SelfMuted = *packet.SelfMute 585 | } 586 | if packet.SelfDeaf != nil { 587 | if *packet.SelfDeaf != user.SelfDeafened { 588 | event.Type |= UserChangeAudio 589 | } 590 | user.SelfDeafened = *packet.SelfDeaf 591 | } 592 | if packet.Texture != nil { 593 | event.Type |= UserChangeTexture 594 | user.Texture = packet.Texture 595 | user.TextureHash = nil 596 | } 597 | if packet.Comment != nil { 598 | if *packet.Comment != user.Comment { 599 | event.Type |= UserChangeComment 600 | } 601 | user.Comment = *packet.Comment 602 | user.CommentHash = nil 603 | } 604 | if packet.Hash != nil { 605 | user.Hash = *packet.Hash 606 | } 607 | if packet.CommentHash != nil { 608 | event.Type |= UserChangeComment 609 | user.CommentHash = packet.CommentHash 610 | user.Comment = "" 611 | } 612 | if packet.TextureHash != nil { 613 | event.Type |= UserChangeTexture 614 | user.TextureHash = packet.TextureHash 615 | user.Texture = nil 616 | } 617 | if packet.PrioritySpeaker != nil { 618 | if *packet.PrioritySpeaker != user.PrioritySpeaker { 619 | event.Type |= UserChangePrioritySpeaker 620 | } 621 | user.PrioritySpeaker = *packet.PrioritySpeaker 622 | } 623 | if packet.Recording != nil { 624 | if *packet.Recording != user.Recording { 625 | event.Type |= UserChangeRecording 626 | } 627 | user.Recording = *packet.Recording 628 | } 629 | 630 | c.volatile.Unlock() 631 | } 632 | 633 | if c.State() == StateSynced { 634 | c.Config.Listeners.onUserChange(&event) 635 | } 636 | return nil 637 | } 638 | 639 | func (c *Client) handleBanList(buffer []byte) error { 640 | var packet MumbleProto.BanList 641 | if err := proto.Unmarshal(buffer, &packet); err != nil { 642 | return err 643 | } 644 | 645 | event := BanListEvent{ 646 | Client: c, 647 | BanList: make(BanList, 0, len(packet.Bans)), 648 | } 649 | 650 | for _, banPacket := range packet.Bans { 651 | ban := &Ban{ 652 | Address: net.IP(banPacket.Address), 653 | } 654 | if banPacket.Mask != nil { 655 | size := net.IPv4len * 8 656 | if len(ban.Address) == net.IPv6len { 657 | size = net.IPv6len * 8 658 | } 659 | ban.Mask = net.CIDRMask(int(*banPacket.Mask), size) 660 | } 661 | if banPacket.Name != nil { 662 | ban.Name = *banPacket.Name 663 | } 664 | if banPacket.Hash != nil { 665 | ban.Hash = *banPacket.Hash 666 | } 667 | if banPacket.Reason != nil { 668 | ban.Reason = *banPacket.Reason 669 | } 670 | if banPacket.Start != nil { 671 | ban.Start, _ = time.Parse(time.RFC3339, *banPacket.Start) 672 | } 673 | if banPacket.Duration != nil { 674 | ban.Duration = time.Duration(*banPacket.Duration) * time.Second 675 | } 676 | event.BanList = append(event.BanList, ban) 677 | } 678 | 679 | c.Config.Listeners.onBanList(&event) 680 | return nil 681 | } 682 | 683 | func (c *Client) handleTextMessage(buffer []byte) error { 684 | var packet MumbleProto.TextMessage 685 | if err := proto.Unmarshal(buffer, &packet); err != nil { 686 | return err 687 | } 688 | 689 | event := TextMessageEvent{ 690 | Client: c, 691 | } 692 | if packet.Actor != nil { 693 | event.Sender = c.Users[*packet.Actor] 694 | } 695 | if packet.Session != nil { 696 | event.Users = make([]*User, 0, len(packet.Session)) 697 | for _, session := range packet.Session { 698 | if user := c.Users[session]; user != nil { 699 | event.Users = append(event.Users, user) 700 | } 701 | } 702 | } 703 | if packet.ChannelId != nil { 704 | event.Channels = make([]*Channel, 0, len(packet.ChannelId)) 705 | for _, id := range packet.ChannelId { 706 | if channel := c.Channels[id]; channel != nil { 707 | event.Channels = append(event.Channels, channel) 708 | } 709 | } 710 | } 711 | if packet.TreeId != nil { 712 | event.Trees = make([]*Channel, 0, len(packet.TreeId)) 713 | for _, id := range packet.TreeId { 714 | if channel := c.Channels[id]; channel != nil { 715 | event.Trees = append(event.Trees, channel) 716 | } 717 | } 718 | } 719 | if packet.Message != nil { 720 | event.Message = *packet.Message 721 | } 722 | 723 | c.Config.Listeners.onTextMessage(&event) 724 | return nil 725 | } 726 | 727 | func (c *Client) handlePermissionDenied(buffer []byte) error { 728 | var packet MumbleProto.PermissionDenied 729 | if err := proto.Unmarshal(buffer, &packet); err != nil { 730 | return err 731 | } 732 | 733 | if packet.Type == nil || *packet.Type == MumbleProto.PermissionDenied_H9K { 734 | return errInvalidProtobuf 735 | } 736 | 737 | event := PermissionDeniedEvent{ 738 | Client: c, 739 | Type: PermissionDeniedType(*packet.Type), 740 | } 741 | if packet.Reason != nil { 742 | event.String = *packet.Reason 743 | } 744 | if packet.Name != nil { 745 | event.String = *packet.Name 746 | } 747 | if packet.Session != nil { 748 | event.User = c.Users[*packet.Session] 749 | if event.User == nil { 750 | return errInvalidProtobuf 751 | } 752 | } 753 | if packet.ChannelId != nil { 754 | event.Channel = c.Channels[*packet.ChannelId] 755 | if event.Channel == nil { 756 | return errInvalidProtobuf 757 | } 758 | } 759 | if packet.Permission != nil { 760 | event.Permission = Permission(*packet.Permission) 761 | } 762 | 763 | c.Config.Listeners.onPermissionDenied(&event) 764 | return nil 765 | } 766 | 767 | func (c *Client) handleACL(buffer []byte) error { 768 | var packet MumbleProto.ACL 769 | if err := proto.Unmarshal(buffer, &packet); err != nil { 770 | return err 771 | } 772 | 773 | acl := &ACL{ 774 | Inherits: packet.GetInheritAcls(), 775 | } 776 | if packet.ChannelId == nil { 777 | return errInvalidProtobuf 778 | } 779 | acl.Channel = c.Channels[*packet.ChannelId] 780 | if acl.Channel == nil { 781 | return errInvalidProtobuf 782 | } 783 | 784 | if packet.Groups != nil { 785 | acl.Groups = make([]*ACLGroup, 0, len(packet.Groups)) 786 | for _, group := range packet.Groups { 787 | aclGroup := &ACLGroup{ 788 | Name: *group.Name, 789 | Inherited: group.GetInherited(), 790 | InheritUsers: group.GetInherit(), 791 | Inheritable: group.GetInheritable(), 792 | } 793 | if group.Add != nil { 794 | aclGroup.UsersAdd = make(map[uint32]*ACLUser) 795 | for _, userID := range group.Add { 796 | aclGroup.UsersAdd[userID] = &ACLUser{ 797 | UserID: userID, 798 | } 799 | } 800 | } 801 | if group.Remove != nil { 802 | aclGroup.UsersRemove = make(map[uint32]*ACLUser) 803 | for _, userID := range group.Remove { 804 | aclGroup.UsersRemove[userID] = &ACLUser{ 805 | UserID: userID, 806 | } 807 | } 808 | } 809 | if group.InheritedMembers != nil { 810 | aclGroup.UsersInherited = make(map[uint32]*ACLUser) 811 | for _, userID := range group.InheritedMembers { 812 | aclGroup.UsersInherited[userID] = &ACLUser{ 813 | UserID: userID, 814 | } 815 | } 816 | } 817 | acl.Groups = append(acl.Groups, aclGroup) 818 | } 819 | } 820 | if packet.Acls != nil { 821 | acl.Rules = make([]*ACLRule, 0, len(packet.Acls)) 822 | for _, rule := range packet.Acls { 823 | aclRule := &ACLRule{ 824 | AppliesCurrent: rule.GetApplyHere(), 825 | AppliesChildren: rule.GetApplySubs(), 826 | Inherited: rule.GetInherited(), 827 | Granted: Permission(rule.GetGrant()), 828 | Denied: Permission(rule.GetDeny()), 829 | } 830 | if rule.UserId != nil { 831 | aclRule.User = &ACLUser{ 832 | UserID: *rule.UserId, 833 | } 834 | } else if rule.Group != nil { 835 | var group *ACLGroup 836 | for _, g := range acl.Groups { 837 | if g.Name == *rule.Group { 838 | group = g 839 | break 840 | } 841 | } 842 | if group == nil { 843 | group = &ACLGroup{ 844 | Name: *rule.Group, 845 | } 846 | } 847 | aclRule.Group = group 848 | } 849 | acl.Rules = append(acl.Rules, aclRule) 850 | } 851 | } 852 | c.tmpACL = acl 853 | return nil 854 | } 855 | 856 | func (c *Client) handleQueryUsers(buffer []byte) error { 857 | var packet MumbleProto.QueryUsers 858 | if err := proto.Unmarshal(buffer, &packet); err != nil { 859 | return err 860 | } 861 | 862 | acl := c.tmpACL 863 | if acl == nil { 864 | return errIncompleteProtobuf 865 | } 866 | c.tmpACL = nil 867 | 868 | userMap := make(map[uint32]string) 869 | for i := 0; i < len(packet.Ids) && i < len(packet.Names); i++ { 870 | userMap[packet.Ids[i]] = packet.Names[i] 871 | } 872 | 873 | for _, group := range acl.Groups { 874 | for _, user := range group.UsersAdd { 875 | user.Name = userMap[user.UserID] 876 | } 877 | for _, user := range group.UsersRemove { 878 | user.Name = userMap[user.UserID] 879 | } 880 | for _, user := range group.UsersInherited { 881 | user.Name = userMap[user.UserID] 882 | } 883 | } 884 | for _, rule := range acl.Rules { 885 | if rule.User != nil { 886 | rule.User.Name = userMap[rule.User.UserID] 887 | } 888 | } 889 | 890 | event := ACLEvent{ 891 | Client: c, 892 | ACL: acl, 893 | } 894 | c.Config.Listeners.onACL(&event) 895 | return nil 896 | } 897 | 898 | func (c *Client) handleCryptSetup(buffer []byte) error { 899 | return errUnimplementedHandler 900 | } 901 | 902 | func (c *Client) handleContextActionModify(buffer []byte) error { 903 | var packet MumbleProto.ContextActionModify 904 | if err := proto.Unmarshal(buffer, &packet); err != nil { 905 | return err 906 | } 907 | 908 | if packet.Action == nil || packet.Operation == nil { 909 | return errInvalidProtobuf 910 | } 911 | 912 | event := ContextActionChangeEvent{ 913 | Client: c, 914 | } 915 | 916 | { 917 | c.volatile.Lock() 918 | 919 | switch *packet.Operation { 920 | case MumbleProto.ContextActionModify_Add: 921 | if ca := c.ContextActions[*packet.Action]; ca != nil { 922 | c.volatile.Unlock() 923 | return nil 924 | } 925 | event.Type = ContextActionAdd 926 | contextAction := c.ContextActions.create(*packet.Action) 927 | if packet.Text != nil { 928 | contextAction.Label = *packet.Text 929 | } 930 | if packet.Context != nil { 931 | contextAction.Type = ContextActionType(*packet.Context) 932 | } 933 | event.ContextAction = contextAction 934 | case MumbleProto.ContextActionModify_Remove: 935 | contextAction := c.ContextActions[*packet.Action] 936 | if contextAction == nil { 937 | c.volatile.Unlock() 938 | return nil 939 | } 940 | event.Type = ContextActionRemove 941 | delete(c.ContextActions, *packet.Action) 942 | event.ContextAction = contextAction 943 | default: 944 | c.volatile.Unlock() 945 | return errInvalidProtobuf 946 | } 947 | 948 | c.volatile.Unlock() 949 | } 950 | 951 | c.Config.Listeners.onContextActionChange(&event) 952 | return nil 953 | } 954 | 955 | func (c *Client) handleContextAction(buffer []byte) error { 956 | return errUnimplementedHandler 957 | } 958 | 959 | func (c *Client) handleUserList(buffer []byte) error { 960 | var packet MumbleProto.UserList 961 | if err := proto.Unmarshal(buffer, &packet); err != nil { 962 | return err 963 | } 964 | 965 | event := UserListEvent{ 966 | Client: c, 967 | UserList: make(RegisteredUsers, 0, len(packet.Users)), 968 | } 969 | 970 | for _, user := range packet.Users { 971 | registeredUser := &RegisteredUser{ 972 | UserID: *user.UserId, 973 | } 974 | if user.Name != nil { 975 | registeredUser.Name = *user.Name 976 | } 977 | if user.LastSeen != nil { 978 | registeredUser.LastSeen, _ = time.ParseInLocation(time.RFC3339, *user.LastSeen, nil) 979 | } 980 | if user.LastChannel != nil { 981 | if lastChannel := c.Channels[*user.LastChannel]; lastChannel != nil { 982 | registeredUser.LastChannel = lastChannel 983 | } 984 | } 985 | event.UserList = append(event.UserList, registeredUser) 986 | } 987 | 988 | c.Config.Listeners.onUserList(&event) 989 | return nil 990 | } 991 | 992 | func (c *Client) handleVoiceTarget(buffer []byte) error { 993 | return errUnimplementedHandler 994 | } 995 | 996 | func (c *Client) handlePermissionQuery(buffer []byte) error { 997 | var packet MumbleProto.PermissionQuery 998 | if err := proto.Unmarshal(buffer, &packet); err != nil { 999 | return err 1000 | } 1001 | 1002 | var singleChannel *Channel 1003 | if packet.ChannelId != nil && packet.Permissions != nil { 1004 | singleChannel = c.Channels[*packet.ChannelId] 1005 | if singleChannel == nil { 1006 | return errInvalidProtobuf 1007 | } 1008 | } 1009 | 1010 | var changedChannels []*Channel 1011 | 1012 | { 1013 | c.volatile.Lock() 1014 | 1015 | if packet.GetFlush() { 1016 | oldPermissions := c.permissions 1017 | c.permissions = make(map[uint32]*Permission) 1018 | changedChannels = make([]*Channel, 0, len(oldPermissions)) 1019 | for channelID := range oldPermissions { 1020 | changedChannels = append(changedChannels, c.Channels[channelID]) 1021 | } 1022 | } 1023 | 1024 | if singleChannel != nil { 1025 | p := Permission(*packet.Permissions) 1026 | c.permissions[singleChannel.ID] = &p 1027 | changedChannels = append(changedChannels, singleChannel) 1028 | } 1029 | 1030 | c.volatile.Unlock() 1031 | } 1032 | 1033 | for _, channel := range changedChannels { 1034 | event := ChannelChangeEvent{ 1035 | Client: c, 1036 | Type: ChannelChangePermission, 1037 | Channel: channel, 1038 | } 1039 | c.Config.Listeners.onChannelChange(&event) 1040 | } 1041 | 1042 | return nil 1043 | } 1044 | 1045 | func (c *Client) handleCodecVersion(buffer []byte) error { 1046 | var packet MumbleProto.CodecVersion 1047 | if err := proto.Unmarshal(buffer, &packet); err != nil { 1048 | return err 1049 | } 1050 | event := ServerConfigEvent{ 1051 | Client: c, 1052 | } 1053 | event.CodecAlpha = packet.Alpha 1054 | event.CodecBeta = packet.Beta 1055 | { 1056 | val := packet.GetPreferAlpha() 1057 | event.CodecPreferAlpha = &val 1058 | } 1059 | { 1060 | val := packet.GetOpus() 1061 | event.CodecOpus = &val 1062 | } 1063 | 1064 | var codec AudioCodec 1065 | switch { 1066 | case *event.CodecOpus: 1067 | codec = getAudioCodec(audioCodecIDOpus) 1068 | } 1069 | if codec != nil { 1070 | c.audioCodec = codec 1071 | 1072 | { 1073 | c.volatile.Lock() 1074 | 1075 | c.AudioEncoder = codec.NewEncoder() 1076 | 1077 | c.volatile.Unlock() 1078 | } 1079 | } 1080 | 1081 | c.Config.Listeners.onServerConfig(&event) 1082 | return nil 1083 | } 1084 | 1085 | func (c *Client) handleUserStats(buffer []byte) error { 1086 | var packet MumbleProto.UserStats 1087 | if err := proto.Unmarshal(buffer, &packet); err != nil { 1088 | return err 1089 | } 1090 | 1091 | if packet.Session == nil { 1092 | return errIncompleteProtobuf 1093 | } 1094 | user := c.Users[*packet.Session] 1095 | if user == nil { 1096 | return errInvalidProtobuf 1097 | } 1098 | 1099 | { 1100 | c.volatile.Lock() 1101 | 1102 | if user.Stats == nil { 1103 | user.Stats = &UserStats{} 1104 | } 1105 | *user.Stats = UserStats{ 1106 | User: user, 1107 | } 1108 | stats := user.Stats 1109 | 1110 | if packet.FromClient != nil { 1111 | if packet.FromClient.Good != nil { 1112 | stats.FromClient.Good = *packet.FromClient.Good 1113 | } 1114 | if packet.FromClient.Late != nil { 1115 | stats.FromClient.Late = *packet.FromClient.Late 1116 | } 1117 | if packet.FromClient.Lost != nil { 1118 | stats.FromClient.Lost = *packet.FromClient.Lost 1119 | } 1120 | if packet.FromClient.Resync != nil { 1121 | stats.FromClient.Resync = *packet.FromClient.Resync 1122 | } 1123 | } 1124 | if packet.FromServer != nil { 1125 | if packet.FromServer.Good != nil { 1126 | stats.FromServer.Good = *packet.FromServer.Good 1127 | } 1128 | if packet.FromClient.Late != nil { 1129 | stats.FromServer.Late = *packet.FromServer.Late 1130 | } 1131 | if packet.FromClient.Lost != nil { 1132 | stats.FromServer.Lost = *packet.FromServer.Lost 1133 | } 1134 | if packet.FromClient.Resync != nil { 1135 | stats.FromServer.Resync = *packet.FromServer.Resync 1136 | } 1137 | } 1138 | 1139 | if packet.UdpPackets != nil { 1140 | stats.UDPPackets = *packet.UdpPackets 1141 | } 1142 | if packet.UdpPingAvg != nil { 1143 | stats.UDPPingAverage = *packet.UdpPingAvg 1144 | } 1145 | if packet.UdpPingVar != nil { 1146 | stats.UDPPingVariance = *packet.UdpPingVar 1147 | } 1148 | if packet.TcpPackets != nil { 1149 | stats.TCPPackets = *packet.TcpPackets 1150 | } 1151 | if packet.TcpPingAvg != nil { 1152 | stats.TCPPingAverage = *packet.TcpPingAvg 1153 | } 1154 | if packet.TcpPingVar != nil { 1155 | stats.TCPPingVariance = *packet.TcpPingVar 1156 | } 1157 | 1158 | if packet.Version != nil { 1159 | stats.Version = parseVersion(packet.Version) 1160 | } 1161 | if packet.Onlinesecs != nil { 1162 | stats.Connected = time.Now().Add(time.Duration(*packet.Onlinesecs) * -time.Second) 1163 | } 1164 | if packet.Idlesecs != nil { 1165 | stats.Idle = time.Duration(*packet.Idlesecs) * time.Second 1166 | } 1167 | if packet.Bandwidth != nil { 1168 | stats.Bandwidth = int(*packet.Bandwidth) 1169 | } 1170 | if packet.Address != nil { 1171 | stats.IP = net.IP(packet.Address) 1172 | } 1173 | if packet.Certificates != nil { 1174 | stats.Certificates = make([]*x509.Certificate, 0, len(packet.Certificates)) 1175 | for _, data := range packet.Certificates { 1176 | if data != nil { 1177 | if cert, err := x509.ParseCertificate(data); err == nil { 1178 | stats.Certificates = append(stats.Certificates, cert) 1179 | } 1180 | } 1181 | } 1182 | } 1183 | stats.StrongCertificate = packet.GetStrongCertificate() 1184 | stats.CELTVersions = packet.GetCeltVersions() 1185 | if packet.Opus != nil { 1186 | stats.Opus = *packet.Opus 1187 | } 1188 | 1189 | c.volatile.Unlock() 1190 | } 1191 | 1192 | event := UserChangeEvent{ 1193 | Client: c, 1194 | Type: UserChangeStats, 1195 | User: user, 1196 | } 1197 | 1198 | c.Config.Listeners.onUserChange(&event) 1199 | return nil 1200 | } 1201 | 1202 | func (c *Client) handleRequestBlob(buffer []byte) error { 1203 | return errUnimplementedHandler 1204 | } 1205 | 1206 | func (c *Client) handleServerConfig(buffer []byte) error { 1207 | var packet MumbleProto.ServerConfig 1208 | if err := proto.Unmarshal(buffer, &packet); err != nil { 1209 | return err 1210 | } 1211 | event := ServerConfigEvent{ 1212 | Client: c, 1213 | } 1214 | if packet.MaxBandwidth != nil { 1215 | val := int(*packet.MaxBandwidth) 1216 | event.MaximumBitrate = &val 1217 | } 1218 | if packet.WelcomeText != nil { 1219 | event.WelcomeMessage = packet.WelcomeText 1220 | } 1221 | if packet.AllowHtml != nil { 1222 | event.AllowHTML = packet.AllowHtml 1223 | } 1224 | if packet.MessageLength != nil { 1225 | val := int(*packet.MessageLength) 1226 | event.MaximumMessageLength = &val 1227 | } 1228 | if packet.ImageMessageLength != nil { 1229 | val := int(*packet.ImageMessageLength) 1230 | event.MaximumImageMessageLength = &val 1231 | } 1232 | if packet.MaxUsers != nil { 1233 | val := int(*packet.MaxUsers) 1234 | event.MaximumUsers = &val 1235 | } 1236 | c.Config.Listeners.onServerConfig(&event) 1237 | return nil 1238 | } 1239 | 1240 | func (c *Client) handleSuggestConfig(buffer []byte) error { 1241 | var packet MumbleProto.SuggestConfig 1242 | if err := proto.Unmarshal(buffer, &packet); err != nil { 1243 | return err 1244 | } 1245 | event := ServerConfigEvent{ 1246 | Client: c, 1247 | } 1248 | if packet.Version != nil { 1249 | event.SuggestVersion = &Version{ 1250 | Version: packet.GetVersion(), 1251 | } 1252 | } 1253 | if packet.Positional != nil { 1254 | event.SuggestPositional = packet.Positional 1255 | } 1256 | if packet.PushToTalk != nil { 1257 | event.SuggestPushToTalk = packet.PushToTalk 1258 | } 1259 | c.Config.Listeners.onServerConfig(&event) 1260 | return nil 1261 | } 1262 | --------------------------------------------------------------------------------