package bmumble import ( "crypto/tls" "crypto/x509" "errors" "fmt" "io/ioutil" "net" "strconv" "strings" "time" "layeh.com/gumble/gumble" "layeh.com/gumble/gumbleutil" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/helper" stripmd "github.com/writeas/go-strip-markdown" // We need to import the 'data' package as an implicit dependency. // See: https://godoc.org/github.com/paulrosania/go-charset/charset _ "github.com/paulrosania/go-charset/data" ) type Bmumble struct { client *gumble.Client Nick string Host string Channel *uint32 local chan config.Message running chan error connected chan gumble.DisconnectEvent serverConfigUpdate chan gumble.ServerConfigEvent serverConfig gumble.ServerConfigEvent tlsConfig tls.Config *bridge.Config } func New(cfg *bridge.Config) bridge.Bridger { b := &Bmumble{} b.Config = cfg b.Nick = b.GetString("Nick") b.local = make(chan config.Message) b.running = make(chan error) b.connected = make(chan gumble.DisconnectEvent) b.serverConfigUpdate = make(chan gumble.ServerConfigEvent) return b } func (b *Bmumble) Connect() error { b.Log.Infof("Connecting %s", b.GetString("Server")) host, portstr, err := net.SplitHostPort(b.GetString("Server")) if err != nil { return err } b.Host = host _, err = strconv.Atoi(portstr) if err != nil { return err } if err = b.buildTLSConfig(); err != nil { return err } go b.doSend() go b.connectLoop() err = <-b.running return err } func (b *Bmumble) Disconnect() error { return b.client.Disconnect() } func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error { cid, err := strconv.ParseUint(channel.Name, 10, 32) if err != nil { return err } channelID := uint32(cid) if b.Channel != nil && *b.Channel != channelID { b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel) return errors.New("the Mumble bridge can only join a single channel") } b.Channel = &channelID return b.doJoin(b.client, channelID) } func (b *Bmumble) Send(msg config.Message) (string, error) { // Only process text messages b.Log.Debugf("=> Received local message %#v", msg) if msg.Event != "" && msg.Event != config.EventUserAction { return "", nil } attachments := b.extractFiles(&msg) b.local <- msg for _, a := range attachments { b.local <- a } return "", nil } func (b *Bmumble) buildTLSConfig() error { b.tlsConfig = tls.Config{} // Load TLS client certificate keypair required for registered user authentication if cpath := b.GetString("TLSClientCertificate"); cpath != "" { if ckey := b.GetString("TLSClientKey"); ckey != "" { cert, err := tls.LoadX509KeyPair(cpath, ckey) if err != nil { return err } b.tlsConfig.Certificates = []tls.Certificate{cert} } } // Load TLS CA used for server verification. If not provided, the Go system trust anchor is used if capath := b.GetString("TLSCACertificate"); capath != "" { ca, err := ioutil.ReadFile(capath) if err != nil { return err } b.tlsConfig.RootCAs = x509.NewCertPool() b.tlsConfig.RootCAs.AppendCertsFromPEM(ca) } b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify") return nil } func (b *Bmumble) connectLoop() { firstConnect := true for { err := b.doConnect() if firstConnect { b.running <- err } if err != nil { b.Log.Errorf("Connection to server failed: %#v", err) if firstConnect { break } else { b.Log.Info("Retrying in 10s") time.Sleep(10 * time.Second) continue } } firstConnect = false d := <-b.connected switch d.Type { case gumble.DisconnectError: b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String) continue case gumble.DisconnectKicked: b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String) continue case gumble.DisconnectBanned: b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String) close(b.connected) close(b.running) return case gumble.DisconnectUser: b.Log.Infof("Disconnect successful") close(b.connected) close(b.running) return } } } func (b *Bmumble) doConnect() error { // Create new gumble config and attach event handlers gumbleConfig := gumble.NewConfig() gumbleConfig.Attach(gumbleutil.Listener{ ServerConfig: b.handleServerConfig, TextMessage: b.handleTextMessage, Connect: b.handleConnect, Disconnect: b.handleDisconnect, UserChange: b.handleUserChange, }) gumbleConfig.Username = b.GetString("Nick") if password := b.GetString("Password"); password != "" { gumbleConfig.Password = password } client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig) if err != nil { return err } b.client = client return nil } func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error { channel, ok := client.Channels[channelID] if !ok { return fmt.Errorf("no channel with ID %d", channelID) } client.Self.Move(channel) return nil } func (b *Bmumble) doSend() { // Message sending loop that makes sure server-side // restrictions and client-side message traits don't conflict // with each other. for { select { case serverConfig := <-b.serverConfigUpdate: b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength) b.serverConfig = serverConfig case msg := <-b.local: b.processMessage(&msg) } } } func (b *Bmumble) processMessage(msg *config.Message) { b.Log.Debugf("Processing message %s", msg.Text) allowHTML := true if b.serverConfig.AllowHTML != nil { allowHTML = *b.serverConfig.AllowHTML } // If this is a specially generated image message, send it unmodified if msg.Event == "mumble_image" { if allowHTML { b.client.Self.Channel.Send(msg.Username+msg.Text, false) } else { b.Log.Info("Can't send image, server does not allow HTML messages") } return } // Don't process empty messages if len(msg.Text) == 0 { return } // If HTML is allowed, convert markdown into HTML, otherwise strip markdown if allowHTML { msg.Text = helper.ParseMarkdown(msg.Text) } else { msg.Text = stripmd.Strip(msg.Text) } // If there is a maximum message length, split and truncate the lines var msgLines []string if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil { msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped")) } else { msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) } // Send the individual lines for i := range msgLines { // Remove unnecessary newline character, since either way we're sending it as individual lines msgLines[i] = strings.TrimSuffix(msgLines[i], "\n") b.client.Self.Channel.Send(msg.Username+msgLines[i], false) } }