// Connect connects the IRC client to the server configured in Config.Server. // To enable explicit SSL on the connection to the IRC server, set Config.SSL // to true before calling Connect(). The port will default to 6697 if SSL is // enabled, and 6667 otherwise. // To enable connecting via a proxy server, set Config.Proxy to the proxy URL // (example socks5://localhost:9000) before calling Connect(). // // Upon successful connection, Connected will return true and a REGISTER event // will be fired. This is mostly for internal use; it is suggested that a // handler for the CONNECTED event is used to perform any initial client work // like joining channels and sending messages. func (conn *Conn) Connect() error { conn.mu.Lock() defer conn.mu.Unlock() conn.initialise() if conn.cfg.Server == "" { return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty") } if conn.connected { return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server) } if !hasPort(conn.cfg.Server) { if conn.cfg.SSL { conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697") } else { conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667") } } if conn.cfg.Proxy != "" { proxyURL, err := url.Parse(conn.cfg.Proxy) if err != nil { return err } conn.proxyDialer, err = proxy.FromURL(proxyURL, conn.dialer) if err != nil { return err } logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) if s, err := conn.proxyDialer.Dial("tcp", conn.cfg.Server); err == nil { conn.sock = s } else { return err } } else { logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server) if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil { conn.sock = s } else { return err } } if conn.cfg.SSL { logging.Info("irc.Connect(): Performing SSL handshake.") s := tls.Client(conn.sock, conn.cfg.SSLConfig) if err := s.Handshake(); err != nil { return err } conn.sock = s } conn.postConnect(true) conn.connected = true conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()}) return nil }
// Parse mode strings for a Nick. func (nk *nick) parseModes(modes string) { var modeop bool // true => add mode, false => remove mode for i := 0; i < len(modes); i++ { switch m := modes[i]; m { case '+': modeop = true case '-': modeop = false case 'B': nk.modes.Bot = modeop case 'i': nk.modes.Invisible = modeop case 'o': nk.modes.Oper = modeop case 'w': nk.modes.WallOps = modeop case 'x': nk.modes.HiddenHost = modeop case 'z': nk.modes.SSL = modeop default: logging.Info("Nick.ParseModes(): unknown mode char %c", m) } } }
func (tbmgr *TemporaryBanManager) Import(r io.Reader) error { tbmgr.dataSync.Lock() defer tbmgr.dataSync.Unlock() decoder := gob.NewDecoder(r) var exportVersion uint64 if err := decoder.Decode(&exportVersion); err != nil { return err } switch exportVersion { case 0x0: // Structure: // - Bans = []*TemporaryBan if err := decoder.Decode(tbmgr.data); err != nil { return err } default: return errors.New("Invalid version found in input data") } // Run background handler for each temporary ban if !tbmgr.DisableExpiry { for _, ban := range tbmgr.data.Bans { tbmgr.handleBan(&ban) } } logging.Info("Imported %v bans.", len(tbmgr.data.Bans)) return nil }
func (p *Plugin) dumpBans(target string) { num := 0 // Fetch ban list banlist, err := p.mode.Bans(target) if err != nil { logging.Warn("Could not fetch ban list, old bans won't get handled") return } tbmgr := p.ensureTemporaryBanManager(target) // Save only bans from us for _, ban := range banlist { if ban.Nick != p.bot.Me().Nick { // Not a ban from us (going by the nickname at least) isOldHostmask := false for _, hostmask := range p.OldHostmasks { // TODO - Test this implementation hostmask = regexp.QuoteMeta(hostmask) hostmask = strings.Replace(hostmask, "\\*", ".*", -1) hostmask = strings.Replace(hostmask, "\\?", ".?", -1) if matched, err := regexp.MatchString(hostmask, ban.Src); matched { isOldHostmask = true break } else if err != nil { logging.Error("vpnbot.Plugin: dumpBans regular expression failed: %v", err) break } } if !isOldHostmask { // Not a ban from an old hostmask either continue } } if _, ok := tbmgr.Get(ban.Hostmask); ok { // We already have this ban saved continue } if err := tbmgr.Add(NewTemporaryBan( ban.Nick, ban.Hostmask, ban.Src, "Migrated old ban", 48*time.Hour+ban.Timestamp.Sub(time.Now()))); err != nil { logging.Warn("Could not migrate ban on %v: %v", ban.Hostmask, err) } num++ } if num > 0 { p.syncBans(target) logging.Info("Migrated %v bans", num) } }
func (conn *Conn) Connect() error { conn.mu.Lock() defer conn.mu.Unlock() conn.initialise() if conn.cfg.Server == "" { return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty") } if conn.connected { return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server) } if conn.cfg.SSL { if !hasPort(conn.cfg.Server) { conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697") } logging.Info("irc.Connect(): Connecting to %s with SSL.", conn.cfg.Server) if s, err := tls.DialWithDialer(conn.dialer, "tcp", conn.cfg.Server, conn.cfg.SSLConfig); err == nil { conn.sock = s } else { return err } } else { if !hasPort(conn.cfg.Server) { conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667") } logging.Info("irc.Connect(): Connecting to %s without SSL.", conn.cfg.Server) if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil { conn.sock = s } else { return err } } conn.connected = true conn.postConnect(true) conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()}) return nil }
func (conn *Conn) shutdown() { // Guard against double-call of shutdown() if we get an error in send() // as calling sock.Close() will cause recv() to receive EOF in readstring() conn.mu.Lock() defer conn.mu.Unlock() if !conn.connected { return } logging.Info("irc.shutdown(): Disconnected from server.") conn.connected = false conn.sock.Close() close(conn.die) conn.wg.Wait() // reinit datastructures ready for next connection conn.initialise() conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()}) }
// shutdown tears down all connection-related state. It is called when either // the sending or receiving goroutines encounter an error. func (conn *Conn) shutdown() { // Guard against double-call of shutdown() if we get an error in send() // as calling sock.Close() will cause recv() to receive EOF in readstring() conn.mu.Lock() if !conn.connected { conn.mu.Unlock() return } logging.Info("irc.shutdown(): Disconnected from server.") conn.connected = false conn.sock.Close() close(conn.die) conn.wg.Wait() conn.mu.Unlock() // Dispatch after closing connection but before reinit // so event handlers can still access state information. conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()}) }
// write writes a \r\n terminated line of output to the connected server, // using Hybrid's algorithm to rate limit if conn.cfg.Flood is false. func (conn *Conn) write(line string) error { if !conn.cfg.Flood { if t := conn.rateLimit(len(line)); t != 0 { // sleep for the current line's time value before sending it logging.Info("irc.rateLimit(): Flood! Sleeping for %.2f secs.", t.Seconds()) <-time.After(t) } } if _, err := conn.io.WriteString(line + "\r\n"); err != nil { return err } if err := conn.io.Flush(); err != nil { return err } logging.Debug("-> %s", line) return nil }
// shutdown tears down all connection-related state. It is called when either // the sending or receiving goroutines encounter an error. func (conn *Conn) shutdown() { // Guard against double-call of shutdown() if we get an error in send() // as calling sock.Close() will cause recv() to receive EOF in readstring() conn.mu.Lock() if !conn.connected { conn.mu.Unlock() return } logging.Info("irc.shutdown(): Disconnected from server.") conn.connected = false conn.sock.Close() close(conn.die) // Drain both in and out channels to avoid a deadlock if the buffers // have filled. See TestSendDeadlockOnFullBuffer in connection_test.go. conn.drainIn() conn.drainOut() conn.wg.Wait() conn.mu.Unlock() // Dispatch after closing connection but before reinit // so event handlers can still access state information. conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()}) }
func (plugin *Plugin) OnJoin(conn *client.Conn, line *client.Line) { logging.Info("vpnbot.Plugin: %v joined %v", line.Src, line.Target()) if lastCheck, ok := plugin.lastCheckNicks[line.Nick]; ok && time.Now().Sub(lastCheck) < 15*time.Minute { // There is a check in progress or another one has been done earlier logging.Debug("vpnbot.Plugin: Not checking %v, last check was %v", line.Nick, humanize.Time(plugin.lastCheckNicks[line.Nick])) return } logging.Debug("vpnbot.Plugin: Checking %v...", line.Nick) plugin.lastCheckNicks[line.Nick] = time.Now() // Is this me? if line.Nick == conn.Me().Nick { logging.Debug("vpnbot.Plugin: %v is actually me, skipping.", line.Nick) return } // Nickname == Ident? (9 chars cut) if !strings.HasPrefix(nonWordCharsRegex.ReplaceAllString(line.Nick, ""), strings.TrimLeft(line.Ident, "~")) { logging.Debug("vpnbot.Plugin: %v's nick doesn't match the ident, skipping.", line.Nick) return } // Hostname is masked RDNS vhost/IP? // TODO - Use regex to avoid banning due to similar vhosts if !maskedAddrRegex.MatchString(line.Host) { // Detected custom vHost logging.Debug("vpnbot.Plugin: %v has a custom vhost, skipping.", line.Nick) return } go func() { botNick := line.Nick nobotActivityChan := make(chan string) defer plugin.bot.HandleFunc("privmsg", func(conn *client.Conn, line *client.Line) { if line.Nick == botNick { nobotActivityChan <- "User sent a message" } }).Remove() defer plugin.bot.HandleFunc("noticed", func(conn *client.Conn, line *client.Line) { if line.Nick == botNick { nobotActivityChan <- "User sent a notice" } }).Remove() defer plugin.bot.HandleFunc("part", func(conn *client.Conn, line *client.Line) { if line.Nick == botNick { nobotActivityChan <- "User left" } }).Remove() defer plugin.bot.HandleFunc("part", func(conn *client.Conn, line *client.Line) { if line.Nick == botNick { } if len(line.Args) > 0 { switch line.Args[0] { case "Excess flood": // If this was an excess flood, consider it spam that should // be good to ban anyways nobotActivityChan <- "Excess flood, banning early" banmask := fmt.Sprintf("%v!%v@%v", "*", "*", line.Host) // TODO - Ramp up/down the duration with increasing/decreasing activity of the bots plugin.banGlobal(plugin.generateBan(line.Nick, banmask, "Instant excess flood", 2*24*time.Hour)) default: nobotActivityChan <- "User quit normally" } } }).Remove() // Give nobotActivityChan some time to prove this is not a bot select { case reason := <-nobotActivityChan: logging.Info("vpnbot.Plugin: %v, skipping.", reason) return case <-time.After(1 * time.Second): } // Get WHOIS info info, err := plugin.whois.WhoIs(line.Nick) if err != nil && err != whois.ErrNoSuchNick { logging.Warn("vpnbot.Plugin: Can't get WHOIS info for %v, skipping: %v", line.Nick, err.Error()) return } // Not an oper? if info.IsOperator { logging.Debug("vpnbot.Plugin: %v is operator, skipping.", line.Nick) return } // Not away if info.IsAway { logging.Debug("vpnbot.Plugin: %v is away, skipping.", line.Nick) return } // Realname == Nickname? if info.Realname != line.Nick { logging.Debug( "vpnbot.Plugin: %v's nick doesn't match the realname, skipping.", line.Nick) return } // Count of channels at least 48 if len(info.Channels) < 48 { logging.Debug( "vpnbot.Plugin: %v is not in a high amount of channels, skipping.", line.Nick) return } // Not halfop, op, aop or owner in any channel for _, prefix := range info.Channels { if prefix == '%' || prefix == '@' || prefix == '&' || prefix == '~' { logging.Debug( "vpnbot.Plugin: %v is opped in a channel, skipping.", line.Nick) return } } // Give nobotActivityChan some time to prove this is not a bot select { case reason := <-nobotActivityChan: logging.Info("vpnbot.Plugin: %v, skipping.", reason) return case <-time.After(250 * time.Millisecond): } // More expensive tests below... // Make sure we deal with an unregistered client status := plugin.nickserv.Status(line.Nick)[line.Nick] if status.Error != nil { logging.Warn("vpnbot.Plugin: Can't get auth status for %v, skipping: %v", line.Nick, status.Error) return } if status.Level >= nickserv.Status_IdentifiedViaPassword { logging.Debug("vpnbot.Plugin: %v is identified, skipping.", line.Nick) return } // Give nobotActivityChan some time to prove this is not a bot select { case reason := <-nobotActivityChan: logging.Info("vpnbot.Plugin: %v, skipping.", reason) return case <-time.After(250 * time.Millisecond): } // This is a bot we need to ban! banmask := fmt.Sprintf("%v!%v@%v", "*", "*", line.Host) // TODO - Ramp up/down the duration with increasing/decreasing activity of the bots plugin.banGlobal(plugin.generateBan(line.Nick, banmask, "Known pattern of ban-evading/logging bot", 2*24*time.Hour)) }() }
// Parses mode strings for a channel. func (ch *Channel) ParseModes(modes string, modeargs ...string) { var modeop bool // true => add mode, false => remove mode var modestr string for i := 0; i < len(modes); i++ { switch m := modes[i]; m { case '+': modeop = true modestr = string(m) case '-': modeop = false modestr = string(m) case 'i': ch.Modes.InviteOnly = modeop case 'm': ch.Modes.Moderated = modeop case 'n': ch.Modes.NoExternalMsg = modeop case 'p': ch.Modes.Private = modeop case 'r': ch.Modes.Registered = modeop case 's': ch.Modes.Secret = modeop case 't': ch.Modes.ProtectedTopic = modeop case 'z': ch.Modes.SSLOnly = modeop case 'Z': ch.Modes.AllSSL = modeop case 'O': ch.Modes.OperOnly = modeop case 'k': if modeop && len(modeargs) != 0 { ch.Modes.Key, modeargs = modeargs[0], modeargs[1:] } else if !modeop { ch.Modes.Key = "" } else { logging.Warn("Channel.ParseModes(): not enough arguments to "+ "process MODE %s %s%c", ch.Name, modestr, m) } case 'l': if modeop && len(modeargs) != 0 { ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]) modeargs = modeargs[1:] } else if !modeop { ch.Modes.Limit = 0 } else { logging.Warn("Channel.ParseModes(): not enough arguments to "+ "process MODE %s %s%c", ch.Name, modestr, m) } case 'q', 'a', 'o', 'h', 'v': if len(modeargs) != 0 { if nk, ok := ch.lookup[modeargs[0]]; ok { cp := ch.nicks[nk] switch m { case 'q': cp.Owner = modeop case 'a': cp.Admin = modeop case 'o': cp.Op = modeop case 'h': cp.HalfOp = modeop case 'v': cp.Voice = modeop } modeargs = modeargs[1:] } else { logging.Warn("Channel.ParseModes(): untracked nick %s "+ "received MODE on channel %s", modeargs[0], ch.Name) } } else { logging.Warn("Channel.ParseModes(): not enough arguments to "+ "process MODE %s %s%c", ch.Name, modestr, m) } default: logging.Info("Channel.ParseModes(): unknown mode char %c", m) } } }
// The main program logic. func main() { // Load configuration path from flags flag.Parse() // Initialize the logger logger = glogging.GLogger{} logging.SetLogger(logger) // Check if we're supposed to generate a default config if *generateDefault { logger.Debug("Saving default configuration...") if err := defaultConfiguration.Save(*configPath); err != nil { logger.Error("Failed at saving default configuration: %v", err) os.Exit(1) } logger.Info("Saved default configuration.") os.Exit(0) } // Check if we're supposed to migrate an old config if *migratePath != "" { logger.Debug("Migrating old configuration...") if c, err := LoadV1Config(*migratePath); err != nil { logger.Error("Failed to load old configuration: %v", err) os.Exit(1) } else { newC := c.Migrate() if err := newC.Save(*configPath); err != nil { logger.Error("Migration failed: %v", err) os.Exit(1) } if err := newC.Validate(); err != nil { logger.Warn("Migration successful but found errors while "+ "validating the new configuration, you should fix this before "+ "running the bot: %v", err) os.Exit(2) } } logger.Info("Migration successful.") os.Exit(0) } // Load configuration from configuration path if c, err := Load(*configPath); err != nil { logger.Error("Can't load configuration from %v: %v\n", *configPath, err) os.Exit(1) } else { loadedConfiguration = c } logger.Debug("Loaded configuration will be printed below.") logger.Debug("%#v", loadedConfiguration) // Validate configuration if err := loadedConfiguration.Validate(); err != nil { logger.Error("The configuration is invalid: %v\n", err) os.Exit(2) } // Now initialize the bot logger.Info("Initializing vpnbot %v...", version) b := bot.NewBot( loadedConfiguration.Server.Address, loadedConfiguration.Server.SSL, loadedConfiguration.Nick, loadedConfiguration.Ident, []string{}) b.Conn().Config().Version = fmt.Sprintf("vpnbot/%v", version) b.Conn().Config().Recover = func(conn *client.Conn, line *client.Line) { if err := recover(); err != nil { logging.Error("An internal error occurred: %v\n%v", err, string(debug.Stack())) } } b.Conn().Config().Pass = loadedConfiguration.Server.Password if loadedConfiguration.Name != "" { b.Conn().Config().Me.Name = loadedConfiguration.Name } // Load plugins // TODO - Move this into its own little intelligent loader struct, maybe. isupportPlugin := isupport.Register(b) modePlugin := mode.Register(b, isupportPlugin) nickservPlugin := nickserv.Register(b, modePlugin) nickservPlugin.Username = loadedConfiguration.NickServ.Username nickservPlugin.Password = loadedConfiguration.NickServ.Password nickservPlugin.Channels = loadedConfiguration.Channels switch { case *makeTempBans: // Run in tempban dumping mode // Prepare channels to let us know about dumped bans doneChan := make(map[string]chan interface{}) for _, channel := range loadedConfiguration.Channels { doneChan[strings.ToLower(channel)] = make(chan interface{}, 1) } // Load the tempban dumping plugin dumptempbanPlugin := dumptempban.Register(b, isupportPlugin, modePlugin) dumptempbanPlugin.DumpedBansFunc = func(target string, num int, err error) { if err != nil { logging.Error("Failed to dump bans for %v: %v", target, err) } else { logging.Info("Dumped %v bans for %v successfully.", num, target) } if done, ok := doneChan[strings.ToLower(target)]; ok { done <- nil } } // Start up the bot asynchronously go b.Run() // Wait for all channels to be done for _, done := range doneChan { <-done } b.Quit("Ban dumping done.") default: // Run normally // Load plugins autojoin.Register(b) adminplugin.Register(b, loadedConfiguration.Admins) bots.Register(b, isupportPlugin) tempbanPlugin := tempban.Register(b, isupportPlugin, modePlugin) tempbanPlugin.OldHostmasks = loadedConfiguration.OldHostmasks whoisPlugin := whois.Register(b, isupportPlugin) vpnbotPlugin := vpnbot.Register(b, whoisPlugin, isupportPlugin, tempbanPlugin, nickservPlugin) vpnbotPlugin.Admins = loadedConfiguration.Admins // This is to update the configuration when the bot joins channels b.HandleFunc("join", func(c *client.Conn, line *client.Line) { // Arguments: [ <channel> ] // Make sure this is about us if line.Nick != c.Me().Nick { return } // I don't think method calls are a good idea in a loop channel := line.Target() // See if we already had this channel saved for _, savedChannel := range loadedConfiguration.Channels { if strings.EqualFold(savedChannel, channel) { return // Channel already saved } } // Store this channel logger.Info("Adding %v to configured channels", channel) loadedConfiguration.Channels = append( loadedConfiguration.Channels, channel) // And save to configuration file! loadedConfiguration.Save(*configPath) }) b.HandleFunc("kick", func(c *client.Conn, line *client.Line) { // Arguments: [ <channel>, <nick>, <reason> ] // Make sure this is about us if line.Args[1] != c.Me().Nick { return } // I don't think method calls are a good idea in a loop channel := line.Target() for index, savedChannel := range loadedConfiguration.Channels { if strings.EqualFold(savedChannel, channel) { // Delete the channel logger.Info("Removing %v from configured channels", savedChannel) loadedConfiguration.Channels = append( loadedConfiguration.Channels[0:index], loadedConfiguration.Channels[index+1:]...) // And save to configuration file! loadedConfiguration.Save(*configPath) return } } }) b.HandleFunc("part", func(c *client.Conn, line *client.Line) { // Arguments: [ <channel> (, <reason>) ] // Make sure this is about us if line.Nick != c.Me().Nick { return } // I don't think method calls are a good idea in a loop channel := line.Target() for index, savedChannel := range loadedConfiguration.Channels { if strings.EqualFold(savedChannel, channel) { // Delete the channel logger.Info("Removing %v from configured channels", savedChannel) loadedConfiguration.Channels = append( loadedConfiguration.Channels[0:index], loadedConfiguration.Channels[index+1:]...) // And save to configuration file! loadedConfiguration.Save(*configPath) return } } }) // Run the bot b.Run() } }