| 1 | package birc |
| 2 | |
| 3 | import ( |
| 4 | "crypto/tls" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "hash/crc32" |
| 8 | "io" |
| 9 | "net" |
| 10 | "sort" |
| 11 | "strconv" |
| 12 | "strings" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/lrstanley/girc" |
| 16 | "github.com/matterbridge-org/matterbridge/bridge" |
| 17 | "github.com/matterbridge-org/matterbridge/bridge/config" |
| 18 | "github.com/matterbridge-org/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 | |
| 26 | type Birc struct { |
| 27 | i *girc.Client |
| 28 | Nick string |
| 29 | names map[string][]string |
| 30 | connected chan error |
| 31 | Local chan config.Message // local queue for flood control |
| 32 | FirstConnection, authDone bool |
| 33 | MessageDelay, MessageQueue, MessageLength int |
| 34 | channels map[string]bool |
| 35 | |
| 36 | *bridge.Config |
| 37 | } |
| 38 | |
| 39 | func New(cfg *bridge.Config) bridge.Bridger { |
| 40 | b := &Birc{} |
| 41 | b.Config = cfg |
| 42 | b.Nick = b.GetString("Nick") |
| 43 | b.names = make(map[string][]string) |
| 44 | b.connected = make(chan error) |
| 45 | b.channels = make(map[string]bool) |
| 46 | |
| 47 | if b.GetInt("MessageDelay") == 0 { |
| 48 | b.MessageDelay = 1300 |
| 49 | } else { |
| 50 | b.MessageDelay = b.GetInt("MessageDelay") |
| 51 | } |
| 52 | if b.GetInt("MessageQueue") == 0 { |
| 53 | b.MessageQueue = 30 |
| 54 | } else { |
| 55 | b.MessageQueue = b.GetInt("MessageQueue") |
| 56 | } |
| 57 | if b.GetInt("MessageLength") == 0 { |
| 58 | b.MessageLength = 400 |
| 59 | } else { |
| 60 | b.MessageLength = b.GetInt("MessageLength") |
| 61 | } |
| 62 | b.FirstConnection = true |
| 63 | return b |
| 64 | } |
| 65 | |
| 66 | func (b *Birc) Command(msg *config.Message) string { |
| 67 | if msg.Text == "!users" { |
| 68 | b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames) |
| 69 | b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames) |
| 70 | b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck |
| 71 | } |
| 72 | return "" |
| 73 | } |
| 74 | |
| 75 | func (b *Birc) Connect() error { |
| 76 | if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" { |
| 77 | return errors.New("you can't enable SASL and TLSClientCertificate at the same time") |
| 78 | } |
| 79 | |
| 80 | b.Local = make(chan config.Message, b.MessageQueue+10) |
| 81 | b.Log.Infof("Connecting %s", b.GetString("Server")) |
| 82 | |
| 83 | i, err := b.getClient() |
| 84 | if err != nil { |
| 85 | return err |
| 86 | } |
| 87 | |
| 88 | if b.GetBool("UseSASL") { |
| 89 | i.Config.SASL = &girc.SASLPlain{ |
| 90 | User: b.GetString("NickServNick"), |
| 91 | Pass: b.GetString("NickServPassword"), |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection) |
| 96 | i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth) |
| 97 | i.Handlers.Add(girc.ERR_NOMOTD, b.handleOtherAuth) |
| 98 | i.Handlers.Add(girc.ALL_EVENTS, b.handleOther) |
| 99 | b.i = i |
| 100 | |
| 101 | go b.doConnect() |
| 102 | |
| 103 | err = <-b.connected |
| 104 | if err != nil { |
| 105 | return fmt.Errorf("connection failed %s", err) |
| 106 | } |
| 107 | b.Log.Info("Connection succeeded") |
| 108 | b.FirstConnection = false |
| 109 | if b.GetInt("DebugLevel") == 0 { |
| 110 | i.Handlers.Clear(girc.ALL_EVENTS) |
| 111 | } |
| 112 | go b.doSend() |
| 113 | return nil |
| 114 | } |
| 115 | |
| 116 | func (b *Birc) Disconnect() error { |
| 117 | b.i.Close() |
| 118 | close(b.Local) |
| 119 | return nil |
| 120 | } |
| 121 | |
| 122 | func (b *Birc) JoinChannel(channel config.ChannelInfo) error { |
| 123 | b.channels[channel.Name] = true |
| 124 | // need to check if we have nickserv auth done before joining channels |
| 125 | for { |
| 126 | if b.authDone { |
| 127 | break |
| 128 | } |
| 129 | time.Sleep(time.Second) |
| 130 | } |
| 131 | if channel.Options.Key != "" { |
| 132 | b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name) |
| 133 | b.i.Cmd.JoinKey(channel.Name, channel.Options.Key) |
| 134 | } else { |
| 135 | b.i.Cmd.Join(channel.Name) |
| 136 | } |
| 137 | return nil |
| 138 | } |
| 139 | |
| 140 | func (b *Birc) Send(msg config.Message) (string, error) { |
| 141 | // ignore delete messages |
| 142 | if msg.Event == config.EventMsgDelete { |
| 143 | return "", nil |
| 144 | } |
| 145 | |
| 146 | b.Log.Debugf("=> Receiving %#v", msg) |
| 147 | |
| 148 | // we can be in between reconnects #385 |
| 149 | if !b.i.IsConnected() { |
| 150 | b.Log.Error("Not connected to server, dropping message") |
| 151 | return "", nil |
| 152 | } |
| 153 | |
| 154 | // Execute a command |
| 155 | if strings.HasPrefix(msg.Text, "!") { |
| 156 | b.Command(&msg) |
| 157 | } |
| 158 | |
| 159 | // convert to specified charset |
| 160 | if err := b.handleCharset(&msg); err != nil { |
| 161 | return "", err |
| 162 | } |
| 163 | |
| 164 | // handle files, return if we're done here |
| 165 | if ok := b.handleFiles(&msg); ok { |
| 166 | return "", nil |
| 167 | } |
| 168 | |
| 169 | var msgLines []string |
| 170 | if b.GetBool("StripMarkdown") { |
| 171 | msg.Text = stripmd.Strip(msg.Text) |
| 172 | } |
| 173 | |
| 174 | if b.GetBool("MessageSplit") { |
| 175 | msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped")) |
| 176 | } else { |
| 177 | msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped")) |
| 178 | } |
| 179 | for i := range msgLines { |
| 180 | if len(b.Local) >= b.MessageQueue { |
| 181 | b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local)) |
| 182 | return "", nil |
| 183 | } |
| 184 | |
| 185 | msg.Text = msgLines[i] |
| 186 | b.Local <- msg |
| 187 | } |
| 188 | return "", nil |
| 189 | } |
| 190 | |
| 191 | func (b *Birc) doConnect() { |
| 192 | for { |
| 193 | if err := b.i.Connect(); err != nil { |
| 194 | b.Log.Errorf("disconnect: error: %s", err) |
| 195 | if b.FirstConnection { |
| 196 | b.connected <- err |
| 197 | return |
| 198 | } |
| 199 | } else { |
| 200 | b.Log.Info("disconnect: client requested quit") |
| 201 | } |
| 202 | b.Log.Info("reconnecting in 30 seconds...") |
| 203 | time.Sleep(30 * time.Second) |
| 204 | b.i.Handlers.Clear(girc.RPL_WELCOME) |
| 205 | b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) { |
| 206 | b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels} |
| 207 | // set our correct nick on reconnect if necessary |
| 208 | b.Nick = event.Source.Name |
| 209 | }) |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | // Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-" |
| 214 | func sanitizeNick(nick string) string { |
| 215 | sanitize := func(r rune) rune { |
| 216 | if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) { |
| 217 | return '-' |
| 218 | } |
| 219 | return r |
| 220 | } |
| 221 | return strings.Map(sanitize, nick) |
| 222 | } |
| 223 | |
| 224 | func (b *Birc) doSend() { |
| 225 | rate := time.Millisecond * time.Duration(b.MessageDelay) |
| 226 | throttle := time.NewTicker(rate) |
| 227 | for msg := range b.Local { |
| 228 | <-throttle.C |
| 229 | username := msg.Username |
| 230 | // Optional support for the proposed RELAYMSG extension, described at |
| 231 | // https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md |
| 232 | // nolint:nestif |
| 233 | if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) && |
| 234 | b.GetBool("UseRelayMsg") { |
| 235 | username = sanitizeNick(username) |
| 236 | text := msg.Text |
| 237 | |
| 238 | // Work around girc chomping leading commas on single word messages? |
| 239 | if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') { |
| 240 | text = ":" + text |
| 241 | } |
| 242 | |
| 243 | if msg.Event == config.EventUserAction { |
| 244 | b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck |
| 245 | } else { |
| 246 | b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username) |
| 247 | b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck |
| 248 | } |
| 249 | } else { |
| 250 | if b.GetBool("Colornicks") { |
| 251 | checksum := crc32.ChecksumIEEE([]byte(msg.Username)) |
| 252 | colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes |
| 253 | username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) |
| 254 | } |
| 255 | switch msg.Event { |
| 256 | case config.EventUserAction: |
| 257 | b.i.Cmd.Action(msg.Channel, username+msg.Text) |
| 258 | case config.EventNoticeIRC: |
| 259 | b.Log.Debugf("Sending notice to channel %s", msg.Channel) |
| 260 | b.i.Cmd.Notice(msg.Channel, username+msg.Text) |
| 261 | default: |
| 262 | b.Log.Debugf("Sending to channel %s", msg.Channel) |
| 263 | b.i.Cmd.Message(msg.Channel, username+msg.Text) |
| 264 | } |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | // validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful |
| 270 | func (b *Birc) getClient() (*girc.Client, error) { |
| 271 | server, portstr, err := net.SplitHostPort(b.GetString("Server")) |
| 272 | if err != nil { |
| 273 | return nil, err |
| 274 | } |
| 275 | port, err := strconv.Atoi(portstr) |
| 276 | if err != nil { |
| 277 | return nil, err |
| 278 | } |
| 279 | user := b.GetString("UserName") |
| 280 | if user == "" { |
| 281 | user = b.GetString("Nick") |
| 282 | } |
| 283 | // fix strict user handling of girc |
| 284 | for !girc.IsValidUser(user) { |
| 285 | if len(user) == 1 || len(user) == 0 { |
| 286 | user = "matterbridge" |
| 287 | break |
| 288 | } |
| 289 | user = user[1:] |
| 290 | } |
| 291 | realName := b.GetString("RealName") |
| 292 | if realName == "" { |
| 293 | realName = b.GetString("Nick") |
| 294 | } |
| 295 | |
| 296 | debug := io.Discard |
| 297 | if b.GetInt("DebugLevel") == 2 { |
| 298 | debug = b.Log.Writer() |
| 299 | } |
| 300 | |
| 301 | pingDelay, err := time.ParseDuration(b.GetString("pingdelay")) |
| 302 | if err != nil || pingDelay == 0 { |
| 303 | pingDelay = time.Minute |
| 304 | } |
| 305 | |
| 306 | b.Log.Debugf("setting pingdelay to %s", pingDelay) |
| 307 | |
| 308 | tlsConfig, err := b.getTLSConfig() |
| 309 | if err != nil { |
| 310 | return nil, err |
| 311 | } |
| 312 | |
| 313 | i := girc.New(girc.Config{ |
| 314 | Server: server, |
| 315 | ServerPass: b.GetString("Password"), |
| 316 | Port: port, |
| 317 | Nick: b.GetString("Nick"), |
| 318 | User: user, |
| 319 | Name: realName, |
| 320 | SSL: b.GetBool("UseTLS"), |
| 321 | Bind: b.GetString("Bind"), |
| 322 | TLSConfig: tlsConfig, |
| 323 | PingDelay: pingDelay, |
| 324 | // skip gIRC internal rate limiting, since we have our own throttling |
| 325 | AllowFlood: true, |
| 326 | Debug: debug, |
| 327 | SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil}, |
| 328 | }) |
| 329 | return i, nil |
| 330 | } |
| 331 | |
| 332 | func (b *Birc) endNames(client *girc.Client, event girc.Event) { |
| 333 | channel := event.Params[1] |
| 334 | sort.Strings(b.names[channel]) |
| 335 | maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() |
| 336 | for len(b.names[channel]) > maxNamesPerPost { |
| 337 | b.Remote <- config.Message{ |
| 338 | Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), |
| 339 | Channel: channel, Account: b.Account, |
| 340 | } |
| 341 | b.names[channel] = b.names[channel][maxNamesPerPost:] |
| 342 | } |
| 343 | b.Remote <- config.Message{ |
| 344 | Username: b.Nick, Text: b.formatnicks(b.names[channel]), |
| 345 | Channel: channel, Account: b.Account, |
| 346 | } |
| 347 | b.names[channel] = nil |
| 348 | b.i.Handlers.Clear(girc.RPL_NAMREPLY) |
| 349 | b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) |
| 350 | } |
| 351 | |
| 352 | func (b *Birc) skipPrivMsg(event girc.Event) bool { |
| 353 | // Our nick can be changed |
| 354 | b.Nick = b.i.GetNick() |
| 355 | |
| 356 | // freenode doesn't send 001 as first reply |
| 357 | if event.Command == "NOTICE" && len(event.Params) != 2 { |
| 358 | return true |
| 359 | } |
| 360 | // don't forward queries to the bot |
| 361 | if event.Params[0] == b.Nick { |
| 362 | return true |
| 363 | } |
| 364 | // don't forward message from ourself |
| 365 | if event.Source != nil { |
| 366 | if event.Source.Name == b.Nick { |
| 367 | return true |
| 368 | } |
| 369 | } |
| 370 | // don't forward messages we sent via RELAYMSG |
| 371 | if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick { |
| 372 | return true |
| 373 | } |
| 374 | // This is the old name of the cap sent in spoofed messages; I've kept this in |
| 375 | // for compatibility reasons |
| 376 | if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick { |
| 377 | return true |
| 378 | } |
| 379 | return false |
| 380 | } |
| 381 | |
| 382 | func (b *Birc) nicksPerRow() int { |
| 383 | return 4 |
| 384 | } |
| 385 | |
| 386 | func (b *Birc) storeNames(client *girc.Client, event girc.Event) { |
| 387 | channel := event.Params[2] |
| 388 | b.names[channel] = append( |
| 389 | b.names[channel], |
| 390 | strings.Split(strings.TrimSpace(event.Last()), " ")...) |
| 391 | } |
| 392 | |
| 393 | func (b *Birc) formatnicks(nicks []string) string { |
| 394 | return strings.Join(nicks, ", ") + " currently on IRC" |
| 395 | } |
| 396 | |
| 397 | func (b *Birc) getTLSConfig() (*tls.Config, error) { |
| 398 | server, _, _ := net.SplitHostPort(b.GetString("server")) |
| 399 | |
| 400 | tlsConfig := &tls.Config{ |
| 401 | InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec |
| 402 | ServerName: server, |
| 403 | } |
| 404 | |
| 405 | if filename := b.GetString("TLSClientCertificate"); filename != "" { |
| 406 | cert, err := tls.LoadX509KeyPair(filename, filename) |
| 407 | if err != nil { |
| 408 | return nil, err |
| 409 | } |
| 410 | |
| 411 | tlsConfig.Certificates = []tls.Certificate{cert} |
| 412 | } |
| 413 | |
| 414 | return tlsConfig, nil |
| 415 | } |
| 416 | |