| 1 | package bmumble |
| 2 | |
| 3 | import ( |
| 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 | |
| 27 | type 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 | |
| 42 | func 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 | |
| 53 | func (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 | |
| 75 | func (b *Bmumble) Disconnect() error { |
| 76 | return b.client.Disconnect() |
| 77 | } |
| 78 | |
| 79 | func (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 | |
| 93 | func (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 | |
| 108 | func (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 | |
| 133 | func (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 | |
| 173 | func (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 | |
| 197 | func (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 | |
| 206 | func (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 | |
| 221 | func (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 | |