diff --git a/README.md b/README.md index dedd17a..42761e2 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,15 @@ msg_template: "Alert {{ .Labels.alertname }} on {{ .Labels.instance }} is {{ .St # Set the internal buffer size for alerts received but not yet sent to IRC. alert_buffer_size: 2048 + +# Patterns used to guess whether NickServ is asking us to IDENTIFY +# Note: If you need to change this because the bot is not catching a request +# from a rather common NickServ, please consider sending a PR to update the +# default config instead. +nickserv_identify_patterns: + - "identify via /msg NickServ identify " + - "type /msg NickServ IDENTIFY password" + - "authenticate yourself to services with the IDENTIFY command" ``` Running the bot (assuming *$GOPATH* and *$PATH* are properly setup for go): diff --git a/config.go b/config.go index 0fa2fad..a947390 100644 --- a/config.go +++ b/config.go @@ -46,6 +46,8 @@ type Config struct { MsgOnce bool `yaml:"msg_once_per_alert_group"` UsePrivmsg bool `yaml:"use_privmsg"` AlertBufferSize int `yaml:"alert_buffer_size"` + + NickservIdentifyPatterns []string `yaml:nickserv_identify_patterns` } func LoadConfig(configFile string) (*Config, error) { @@ -64,6 +66,11 @@ func LoadConfig(configFile string) (*Config, error) { MsgOnce: false, UsePrivmsg: false, AlertBufferSize: 2048, + NickservIdentifyPatterns: []string{ + "identify via /msg NickServ identify ", + "type /msg NickServ IDENTIFY password", + "authenticate yourself to services with the IDENTIFY command", + }, } if configFile != "" { diff --git a/irc.go b/irc.go index 7e09b82..63575b0 100644 --- a/irc.go +++ b/irc.go @@ -81,8 +81,11 @@ type IRCNotifier struct { // might change its copy. Nick string NickPassword string - Client *irc.Conn - AlertMsgs chan AlertMsg + + NickservIdentifyPatterns []string + + Client *irc.Conn + AlertMsgs chan AlertMsg // irc.Conn has a Connected() method that can tell us wether the TCP // connection is up, and thus if we should trigger connect/disconnect. @@ -116,17 +119,18 @@ func NewIRCNotifier(config *Config, alertMsgs chan AlertMsg, delayerMaker Delaye channelReconciler := NewChannelReconciler(config, client, delayerMaker, timeTeller) notifier := &IRCNotifier{ - Nick: config.IRCNick, - NickPassword: config.IRCNickPass, - Client: client, - AlertMsgs: alertMsgs, - sessionUpSignal: make(chan bool), - sessionDownSignal: make(chan bool), - channelReconciler: channelReconciler, - UsePrivmsg: config.UsePrivmsg, - NickservDelayWait: nickservWaitSecs * time.Second, - BackoffCounter: backoffCounter, - timeTeller: timeTeller, + Nick: config.IRCNick, + NickPassword: config.IRCNickPass, + NickservIdentifyPatterns: config.NickservIdentifyPatterns, + Client: client, + AlertMsgs: alertMsgs, + sessionUpSignal: make(chan bool), + sessionDownSignal: make(chan bool), + channelReconciler: channelReconciler, + UsePrivmsg: config.UsePrivmsg, + NickservDelayWait: nickservWaitSecs * time.Second, + BackoffCounter: backoffCounter, + timeTeller: timeTeller, } notifier.registerHandlers() @@ -147,18 +151,53 @@ func (n *IRCNotifier) registerHandlers() { n.sessionDownSignal <- false }) - for _, event := range []string{irc.NOTICE, "433"} { + n.Client.HandleFunc(irc.NOTICE, + func(_ *irc.Conn, line *irc.Line) { + n.HandleNotice(line.Nick, line.Text()) + }) + + for _, event := range []string{"433"} { n.Client.HandleFunc(event, loggerHandler) } } -func (n *IRCNotifier) MaybeIdentifyNick() { +func (n *IRCNotifier) HandleNotice(nick string, msg string) { + logging.Info("Received NOTICE from %s: %s", nick, msg) + if strings.ToLower(nick) == "nickserv" { + n.HandleNickservMsg(msg) + } +} + +func (n *IRCNotifier) HandleNickservMsg(msg string) { if n.NickPassword == "" { + logging.Debug("Skip processing NickServ request, no password configured") + return + } + + // Remove most common formatting options from NickServ messages + cleaner := strings.NewReplacer( + "\001", "", // bold + "\002", "", // faint + "\004", "", // underline + ) + cleanedMsg := cleaner.Replace(msg) + + for _, identifyPattern := range n.NickservIdentifyPatterns { + logging.Debug("Checking if NickServ message matches identify request '%s'", identifyPattern) + if strings.Contains(cleanedMsg, identifyPattern) { + logging.Info("Handling NickServ request to IDENTIFY") + n.Client.Privmsgf("NickServ", "IDENTIFY %s", n.NickPassword) + return + } + } +} + +func (n *IRCNotifier) MaybeGhostNick() { + if n.NickPassword == "" { + logging.Debug("Skip GHOST check, no password configured") return } - // Very lazy/optimistic, but this is good enough for my irssi config, - // so it should work here as well. currentNick := n.Client.Me().Nick if currentNick != n.Nick { logging.Info("My nick is '%s', sending GHOST to NickServ to get '%s'", @@ -169,9 +208,19 @@ func (n *IRCNotifier) MaybeIdentifyNick() { logging.Info("Changing nick to '%s'", n.Nick) n.Client.Nick(n.Nick) + time.Sleep(n.NickservDelayWait) } - logging.Info("Sending IDENTIFY to NickServ") - n.Client.Privmsgf("NickServ", "IDENTIFY %s", n.NickPassword) +} + +func (n *IRCNotifier) MaybeWaitForNickserv() { + if n.NickPassword == "" { + logging.Debug("Skip NickServ wait, no password configured") + return + } + + // Very lazy/optimistic, but this is good enough for my irssi config, + // so it should work here as well. + logging.Info("Waiting for NickServ to notice us and issue an identify request") time.Sleep(n.NickservDelayWait) } @@ -261,7 +310,8 @@ func (n *IRCNotifier) SetupPhase(ctx context.Context) { case <-n.sessionUpSignal: n.sessionUp = true n.sessionWg.Add(1) - n.MaybeIdentifyNick() + n.MaybeGhostNick() + n.MaybeWaitForNickserv() n.channelReconciler.Start(ctx) ircConnectedGauge.Set(1) case <-n.sessionDownSignal: diff --git a/irc_test.go b/irc_test.go index 69f239d..b6b06d1 100644 --- a/irc_test.go +++ b/irc_test.go @@ -39,6 +39,9 @@ func makeTestIRCConfig(IRCPort int) *Config { IRCChannel{Name: "#foo"}, }, UsePrivmsg: false, + NickservIdentifyPatterns: []string{ + "identify yourself ktnxbye", + }, } } @@ -468,6 +471,15 @@ func TestIdentify(t *testing.T) { var testStep sync.WaitGroup + // Trigger NickServ identify request when we see the NICK command + // Note: We also test formatting cleanup with this message + nickHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { + var err error + _, err = conn.WriteString(":NickServ!NickServ@services. NOTICE airtest :This nickname is registered. Please choose a different nickname, or \002identify yourself\002 ktnxbye.\n") + return err + } + server.SetHandler("NICK", nickHandler) + // Wait until the pre-joined channel is seen (joining happens // after identification). joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { @@ -500,7 +512,7 @@ func TestIdentify(t *testing.T) { } } -func TestGhostAndIdentify(t *testing.T) { +func TestGhost(t *testing.T) { server, port := makeTestServer(t) config := makeTestIRCConfig(port) config.IRCNickPass = "nickpassword" @@ -530,7 +542,7 @@ func TestGhostAndIdentify(t *testing.T) { server.SetHandler("NICK", nickHandler) // Wait until the pre-joined channel is seen (joining happens - // after identification). + // after ghosting). joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { testStep.Done() return hJOIN(conn, line) @@ -553,7 +565,6 @@ func TestGhostAndIdentify(t *testing.T) { "NICK foo^", "PRIVMSG NickServ :GHOST foo nickpassword", "NICK foo", - "PRIVMSG NickServ :IDENTIFY nickpassword", "PRIVMSG ChanServ :UNBAN #foo", "JOIN #foo", "QUIT :see ya",