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