explicitly handle nickserv identify request

handle nickserv identify requests instead of blindly issuing a message
when connected

this helps if nickserv's state is wiped and we are being asked to
re-identify

introduce a `nickserv_identify_patterns` config option. these patterns
are used to guess identify requests of the various nickserv implementations

Signed-off-by: Luca Bigliardi <shammash@google.com>
This commit is contained in:
Luca Bigliardi 2021-04-16 18:17:08 +02:00
parent 9748c8bfbf
commit a63bfa3aad
4 changed files with 100 additions and 23 deletions

View File

@ -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. # Set the internal buffer size for alerts received but not yet sent to IRC.
alert_buffer_size: 2048 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 <password>"
- "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): Running the bot (assuming *$GOPATH* and *$PATH* are properly setup for go):

View File

@ -46,6 +46,8 @@ type Config struct {
MsgOnce bool `yaml:"msg_once_per_alert_group"` MsgOnce bool `yaml:"msg_once_per_alert_group"`
UsePrivmsg bool `yaml:"use_privmsg"` UsePrivmsg bool `yaml:"use_privmsg"`
AlertBufferSize int `yaml:"alert_buffer_size"` AlertBufferSize int `yaml:"alert_buffer_size"`
NickservIdentifyPatterns []string `yaml:nickserv_identify_patterns`
} }
func LoadConfig(configFile string) (*Config, error) { func LoadConfig(configFile string) (*Config, error) {
@ -64,6 +66,11 @@ func LoadConfig(configFile string) (*Config, error) {
MsgOnce: false, MsgOnce: false,
UsePrivmsg: false, UsePrivmsg: false,
AlertBufferSize: 2048, AlertBufferSize: 2048,
NickservIdentifyPatterns: []string{
"identify via /msg NickServ identify <password>",
"type /msg NickServ IDENTIFY password",
"authenticate yourself to services with the IDENTIFY command",
},
} }
if configFile != "" { if configFile != "" {

90
irc.go
View File

@ -81,8 +81,11 @@ type IRCNotifier struct {
// might change its copy. // might change its copy.
Nick string Nick string
NickPassword 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 // irc.Conn has a Connected() method that can tell us wether the TCP
// connection is up, and thus if we should trigger connect/disconnect. // 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) channelReconciler := NewChannelReconciler(config, client, delayerMaker, timeTeller)
notifier := &IRCNotifier{ notifier := &IRCNotifier{
Nick: config.IRCNick, Nick: config.IRCNick,
NickPassword: config.IRCNickPass, NickPassword: config.IRCNickPass,
Client: client, NickservIdentifyPatterns: config.NickservIdentifyPatterns,
AlertMsgs: alertMsgs, Client: client,
sessionUpSignal: make(chan bool), AlertMsgs: alertMsgs,
sessionDownSignal: make(chan bool), sessionUpSignal: make(chan bool),
channelReconciler: channelReconciler, sessionDownSignal: make(chan bool),
UsePrivmsg: config.UsePrivmsg, channelReconciler: channelReconciler,
NickservDelayWait: nickservWaitSecs * time.Second, UsePrivmsg: config.UsePrivmsg,
BackoffCounter: backoffCounter, NickservDelayWait: nickservWaitSecs * time.Second,
timeTeller: timeTeller, BackoffCounter: backoffCounter,
timeTeller: timeTeller,
} }
notifier.registerHandlers() notifier.registerHandlers()
@ -147,18 +151,53 @@ func (n *IRCNotifier) registerHandlers() {
n.sessionDownSignal <- false 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) 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 == "" { 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 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 currentNick := n.Client.Me().Nick
if currentNick != n.Nick { if currentNick != n.Nick {
logging.Info("My nick is '%s', sending GHOST to NickServ to get '%s'", 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) logging.Info("Changing nick to '%s'", n.Nick)
n.Client.Nick(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) time.Sleep(n.NickservDelayWait)
} }
@ -261,7 +310,8 @@ func (n *IRCNotifier) SetupPhase(ctx context.Context) {
case <-n.sessionUpSignal: case <-n.sessionUpSignal:
n.sessionUp = true n.sessionUp = true
n.sessionWg.Add(1) n.sessionWg.Add(1)
n.MaybeIdentifyNick() n.MaybeGhostNick()
n.MaybeWaitForNickserv()
n.channelReconciler.Start(ctx) n.channelReconciler.Start(ctx)
ircConnectedGauge.Set(1) ircConnectedGauge.Set(1)
case <-n.sessionDownSignal: case <-n.sessionDownSignal:

View File

@ -39,6 +39,9 @@ func makeTestIRCConfig(IRCPort int) *Config {
IRCChannel{Name: "#foo"}, IRCChannel{Name: "#foo"},
}, },
UsePrivmsg: false, UsePrivmsg: false,
NickservIdentifyPatterns: []string{
"identify yourself ktnxbye",
},
} }
} }
@ -468,6 +471,15 @@ func TestIdentify(t *testing.T) {
var testStep sync.WaitGroup 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 // Wait until the pre-joined channel is seen (joining happens
// after identification). // after identification).
joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { 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) server, port := makeTestServer(t)
config := makeTestIRCConfig(port) config := makeTestIRCConfig(port)
config.IRCNickPass = "nickpassword" config.IRCNickPass = "nickpassword"
@ -530,7 +542,7 @@ func TestGhostAndIdentify(t *testing.T) {
server.SetHandler("NICK", nickHandler) server.SetHandler("NICK", nickHandler)
// Wait until the pre-joined channel is seen (joining happens // Wait until the pre-joined channel is seen (joining happens
// after identification). // after ghosting).
joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error { joinHandler := func(conn *bufio.ReadWriter, line *irc.Line) error {
testStep.Done() testStep.Done()
return hJOIN(conn, line) return hJOIN(conn, line)
@ -553,7 +565,6 @@ func TestGhostAndIdentify(t *testing.T) {
"NICK foo^", "NICK foo^",
"PRIVMSG NickServ :GHOST foo nickpassword", "PRIVMSG NickServ :GHOST foo nickpassword",
"NICK foo", "NICK foo",
"PRIVMSG NickServ :IDENTIFY nickpassword",
"PRIVMSG ChanServ :UNBAN #foo", "PRIVMSG ChanServ :UNBAN #foo",
"JOIN #foo", "JOIN #foo",
"QUIT :see ya", "QUIT :see ya",