Thumbnail

rani/matterbridge.git

Clone URL: https://git.buni.party/rani/matterbridge.git

Viewing file on branch master

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