1package bmumble
2
3import (
4	"crypto/tls"
5	"crypto/x509"
6	"errors"
7	"fmt"
8	"io/ioutil"
9	"net"
10	"strconv"
11	"time"
12
13	"layeh.com/gumble/gumble"
14	"layeh.com/gumble/gumbleutil"
15
16	"github.com/42wim/matterbridge/bridge"
17	"github.com/42wim/matterbridge/bridge/config"
18	"github.com/42wim/matterbridge/bridge/helper"
19	stripmd "github.com/writeas/go-strip-markdown"
20
21	// We need to import the 'data' package as an implicit dependency.
22	// See: https://godoc.org/github.com/paulrosania/go-charset/charset
23	_ "github.com/paulrosania/go-charset/data"
24)
25
26type Bmumble struct {
27	client             *gumble.Client
28	Nick               string
29	Host               string
30	Channel            *uint32
31	local              chan config.Message
32	running            chan error
33	connected          chan gumble.DisconnectEvent
34	serverConfigUpdate chan gumble.ServerConfigEvent
35	serverConfig       gumble.ServerConfigEvent
36	tlsConfig          tls.Config
37
38	*bridge.Config
39}
40
41func New(cfg *bridge.Config) bridge.Bridger {
42	b := &Bmumble{}
43	b.Config = cfg
44	b.Nick = b.GetString("Nick")
45	b.local = make(chan config.Message)
46	b.running = make(chan error)
47	b.connected = make(chan gumble.DisconnectEvent)
48	b.serverConfigUpdate = make(chan gumble.ServerConfigEvent)
49	return b
50}
51
52func (b *Bmumble) Connect() error {
53	b.Log.Infof("Connecting %s", b.GetString("Server"))
54	host, portstr, err := net.SplitHostPort(b.GetString("Server"))
55	if err != nil {
56		return err
57	}
58	b.Host = host
59	_, err = strconv.Atoi(portstr)
60	if err != nil {
61		return err
62	}
63
64	if err = b.buildTLSConfig(); err != nil {
65		return err
66	}
67
68	go b.doSend()
69	go b.connectLoop()
70	err = <-b.running
71	return err
72}
73
74func (b *Bmumble) Disconnect() error {
75	return b.client.Disconnect()
76}
77
78func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
79	cid, err := strconv.ParseUint(channel.Name, 10, 32)
80	if err != nil {
81		return err
82	}
83	channelID := uint32(cid)
84	if b.Channel != nil && *b.Channel != channelID {
85		b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel)
86		return errors.New("the Mumble bridge can only join a single channel")
87	}
88	b.Channel = &channelID
89	return b.doJoin(b.client, channelID)
90}
91
92func (b *Bmumble) Send(msg config.Message) (string, error) {
93	// Only process text messages
94	b.Log.Debugf("=> Received local message %#v", msg)
95	if msg.Event != "" && msg.Event != config.EventUserAction {
96		return "", nil
97	}
98
99	attachments := b.extractFiles(&msg)
100	b.local <- msg
101	for _, a := range attachments {
102		b.local <- a
103	}
104	return "", nil
105}
106
107func (b *Bmumble) buildTLSConfig() error {
108	b.tlsConfig = tls.Config{}
109	// Load TLS client certificate keypair required for registered user authentication
110	if cpath := b.GetString("TLSClientCertificate"); cpath != "" {
111		if ckey := b.GetString("TLSClientKey"); ckey != "" {
112			cert, err := tls.LoadX509KeyPair(cpath, ckey)
113			if err != nil {
114				return err
115			}
116			b.tlsConfig.Certificates = []tls.Certificate{cert}
117		}
118	}
119	// Load TLS CA used for server verification.  If not provided, the Go system trust anchor is used
120	if capath := b.GetString("TLSCACertificate"); capath != "" {
121		ca, err := ioutil.ReadFile(capath)
122		if err != nil {
123			return err
124		}
125		b.tlsConfig.RootCAs = x509.NewCertPool()
126		b.tlsConfig.RootCAs.AppendCertsFromPEM(ca)
127	}
128	b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
129	return nil
130}
131
132func (b *Bmumble) connectLoop() {
133	firstConnect := true
134	for {
135		err := b.doConnect()
136		if firstConnect {
137			b.running <- err
138		}
139		if err != nil {
140			b.Log.Errorf("Connection to server failed: %#v", err)
141			if firstConnect {
142				break
143			} else {
144				b.Log.Info("Retrying in 10s")
145				time.Sleep(10 * time.Second)
146				continue
147			}
148		}
149		firstConnect = false
150		d := <-b.connected
151		switch d.Type {
152		case gumble.DisconnectError:
153			b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String)
154			continue
155		case gumble.DisconnectKicked:
156			b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String)
157			continue
158		case gumble.DisconnectBanned:
159			b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String)
160			close(b.connected)
161			close(b.running)
162			return
163		case gumble.DisconnectUser:
164			b.Log.Infof("Disconnect successful")
165			close(b.connected)
166			close(b.running)
167			return
168		}
169	}
170}
171
172func (b *Bmumble) doConnect() error {
173	// Create new gumble config and attach event handlers
174	gumbleConfig := gumble.NewConfig()
175	gumbleConfig.Attach(gumbleutil.Listener{
176		ServerConfig: b.handleServerConfig,
177		TextMessage:  b.handleTextMessage,
178		Connect:      b.handleConnect,
179		Disconnect:   b.handleDisconnect,
180		UserChange:   b.handleUserChange,
181	})
182	gumbleConfig.Username = b.GetString("Nick")
183	if password := b.GetString("Password"); password != "" {
184		gumbleConfig.Password = password
185	}
186
187	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
188	if err != nil {
189		return err
190	}
191	b.client = client
192	return nil
193}
194
195func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error {
196	channel, ok := client.Channels[channelID]
197	if !ok {
198		return fmt.Errorf("no channel with ID %d", channelID)
199	}
200	client.Self.Move(channel)
201	return nil
202}
203
204func (b *Bmumble) doSend() {
205	// Message sending loop that makes sure server-side
206	// restrictions and client-side message traits don't conflict
207	// with each other.
208	for {
209		select {
210		case serverConfig := <-b.serverConfigUpdate:
211			b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength)
212			b.serverConfig = serverConfig
213		case msg := <-b.local:
214			b.processMessage(&msg)
215		}
216	}
217}
218
219func (b *Bmumble) processMessage(msg *config.Message) {
220	b.Log.Debugf("Processing message %s", msg.Text)
221
222	allowHTML := true
223	if b.serverConfig.AllowHTML != nil {
224		allowHTML = *b.serverConfig.AllowHTML
225	}
226
227	// If this is a specially generated image message, send it unmodified
228	if msg.Event == "mumble_image" {
229		if allowHTML {
230			b.client.Self.Channel.Send(msg.Username+msg.Text, false)
231		} else {
232			b.Log.Info("Can't send image, server does not allow HTML messages")
233		}
234		return
235	}
236
237	// Don't process empty messages
238	if len(msg.Text) == 0 {
239		return
240	}
241	// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
242	if allowHTML {
243		msg.Text = helper.ParseMarkdown(msg.Text)
244	} else {
245		msg.Text = stripmd.Strip(msg.Text)
246	}
247
248	// If there is a maximum message length, split and truncate the lines
249	var msgLines []string
250	if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
251		msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
252	} else {
253		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
254	}
255	// Send the individual lindes
256	for i := range msgLines {
257		b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
258	}
259}
260