mirror of
https://github.com/google/alertmanager-irc-relay.git
synced 2024-12-25 12:22:44 +01:00
f5023118ba
Signed-off-by: Luca Bigliardi <shammash@google.com>
337 lines
9.4 KiB
Go
337 lines
9.4 KiB
Go
// Copyright 2018 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
irc "github.com/fluffle/goirc/client"
|
|
"github.com/google/alertmanager-irc-relay/logging"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
)
|
|
|
|
const (
|
|
pingFrequencySecs = 60
|
|
connectionTimeoutSecs = 30
|
|
nickservWaitSecs = 10
|
|
ircConnectMaxBackoffSecs = 300
|
|
ircConnectBackoffResetSecs = 1800
|
|
)
|
|
|
|
var (
|
|
ircConnectedGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
|
Name: "irc_connected",
|
|
Help: "Whether the IRC connection is established",
|
|
})
|
|
ircSentMsgs = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "irc_sent_msgs",
|
|
Help: "Number of IRC messages sent"},
|
|
[]string{"ircchannel"},
|
|
)
|
|
ircSendMsgErrors = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
Name: "irc_send_msg_errors",
|
|
Help: "Errors while sending IRC messages"},
|
|
[]string{"ircchannel", "error"},
|
|
)
|
|
)
|
|
|
|
func loggerHandler(_ *irc.Conn, line *irc.Line) {
|
|
logging.Info("Received: '%s'", line.Raw)
|
|
}
|
|
|
|
func makeGOIRCConfig(config *Config) *irc.Config {
|
|
ircConfig := irc.NewConfig(config.IRCNick)
|
|
ircConfig.Me.Ident = config.IRCNick
|
|
ircConfig.Me.Name = config.IRCRealName
|
|
ircConfig.Server = strings.Join(
|
|
[]string{config.IRCHost, strconv.Itoa(config.IRCPort)}, ":")
|
|
ircConfig.Pass = config.IRCHostPass
|
|
ircConfig.SSL = config.IRCUseSSL
|
|
ircConfig.SSLConfig = &tls.Config{
|
|
ServerName: config.IRCHost,
|
|
InsecureSkipVerify: !config.IRCVerifySSL,
|
|
}
|
|
ircConfig.PingFreq = pingFrequencySecs * time.Second
|
|
ircConfig.Timeout = connectionTimeoutSecs * time.Second
|
|
ircConfig.NewNick = func(n string) string { return n + "^" }
|
|
|
|
return ircConfig
|
|
}
|
|
|
|
type IRCNotifier struct {
|
|
// Nick stores the nickname specified in the config, because irc.Client
|
|
// might change its copy.
|
|
Nick string
|
|
NickPassword string
|
|
|
|
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.
|
|
// We need to track the session establishment also at a higher level to
|
|
// understand when the server has accepted us and thus when we can join
|
|
// channels, send notices, etc.
|
|
sessionUp bool
|
|
sessionUpSignal chan bool
|
|
sessionDownSignal chan bool
|
|
sessionWg sync.WaitGroup
|
|
|
|
channelReconciler *ChannelReconciler
|
|
|
|
UsePrivmsg bool
|
|
|
|
NickservDelayWait time.Duration
|
|
BackoffCounter Delayer
|
|
timeTeller TimeTeller
|
|
}
|
|
|
|
func NewIRCNotifier(config *Config, alertMsgs chan AlertMsg, delayerMaker DelayerMaker, timeTeller TimeTeller) (*IRCNotifier, error) {
|
|
|
|
ircConfig := makeGOIRCConfig(config)
|
|
|
|
client := irc.Client(ircConfig)
|
|
|
|
backoffCounter := delayerMaker.NewDelayer(
|
|
ircConnectMaxBackoffSecs, ircConnectBackoffResetSecs,
|
|
time.Second)
|
|
|
|
channelReconciler := NewChannelReconciler(config, client, delayerMaker, timeTeller)
|
|
|
|
notifier := &IRCNotifier{
|
|
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()
|
|
|
|
return notifier, nil
|
|
}
|
|
|
|
func (n *IRCNotifier) registerHandlers() {
|
|
n.Client.HandleFunc(irc.CONNECTED,
|
|
func(*irc.Conn, *irc.Line) {
|
|
logging.Info("Session established")
|
|
n.sessionUpSignal <- true
|
|
})
|
|
|
|
n.Client.HandleFunc(irc.DISCONNECTED,
|
|
func(*irc.Conn, *irc.Line) {
|
|
logging.Info("Disconnected from IRC")
|
|
n.sessionDownSignal <- false
|
|
})
|
|
|
|
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) 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
|
|
"\037", "", // 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
|
|
}
|
|
|
|
currentNick := n.Client.Me().Nick
|
|
if currentNick != n.Nick {
|
|
logging.Info("My nick is '%s', sending GHOST to NickServ to get '%s'",
|
|
currentNick, n.Nick)
|
|
n.Client.Privmsgf("NickServ", "GHOST %s %s", n.Nick,
|
|
n.NickPassword)
|
|
time.Sleep(n.NickservDelayWait)
|
|
|
|
logging.Info("Changing nick to '%s'", n.Nick)
|
|
n.Client.Nick(n.Nick)
|
|
time.Sleep(n.NickservDelayWait)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (n *IRCNotifier) ChannelJoined(ctx context.Context, channel string) bool {
|
|
|
|
isJoined, waitJoined := n.channelReconciler.JoinChannel(channel)
|
|
if isJoined {
|
|
return true
|
|
}
|
|
|
|
select {
|
|
case <-waitJoined:
|
|
return true
|
|
case <-n.timeTeller.After(ircJoinWaitSecs * time.Second):
|
|
logging.Warn("Channel %s not joined after %d seconds, giving bad news to caller", channel, ircJoinWaitSecs)
|
|
return false
|
|
case <-ctx.Done():
|
|
logging.Info("Context canceled while waiting for join on channel %s", channel)
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (n *IRCNotifier) SendAlertMsg(ctx context.Context, alertMsg *AlertMsg) {
|
|
if !n.sessionUp {
|
|
logging.Error("Cannot send alert to %s : IRC not connected", alertMsg.Channel)
|
|
ircSendMsgErrors.WithLabelValues(alertMsg.Channel, "not_connected").Inc()
|
|
return
|
|
}
|
|
if !n.ChannelJoined(ctx, alertMsg.Channel) {
|
|
logging.Error("Cannot send alert to %s : cannot join channel", alertMsg.Channel)
|
|
ircSendMsgErrors.WithLabelValues(alertMsg.Channel, "not_joined").Inc()
|
|
return
|
|
}
|
|
|
|
if n.UsePrivmsg {
|
|
n.Client.Privmsg(alertMsg.Channel, alertMsg.Alert)
|
|
} else {
|
|
n.Client.Notice(alertMsg.Channel, alertMsg.Alert)
|
|
}
|
|
ircSentMsgs.WithLabelValues(alertMsg.Channel).Inc()
|
|
}
|
|
|
|
func (n *IRCNotifier) ShutdownPhase() {
|
|
if n.sessionUp {
|
|
logging.Info("IRC client connected, quitting")
|
|
n.Client.Quit("see ya")
|
|
|
|
logging.Info("Wait for IRC disconnect to complete")
|
|
select {
|
|
case <-n.sessionDownSignal:
|
|
case <-n.timeTeller.After(n.Client.Config().Timeout):
|
|
logging.Warn("Timeout while waiting for IRC disconnect to complete, stopping anyway")
|
|
}
|
|
n.sessionWg.Done()
|
|
}
|
|
logging.Info("IRC shutdown complete")
|
|
}
|
|
|
|
func (n *IRCNotifier) ConnectedPhase(ctx context.Context) {
|
|
select {
|
|
case alertMsg := <-n.AlertMsgs:
|
|
n.SendAlertMsg(ctx, &alertMsg)
|
|
case <-n.sessionDownSignal:
|
|
n.sessionUp = false
|
|
n.sessionWg.Done()
|
|
n.channelReconciler.Stop()
|
|
n.Client.Quit("see ya")
|
|
ircConnectedGauge.Set(0)
|
|
case <-ctx.Done():
|
|
logging.Info("IRC routine asked to terminate")
|
|
}
|
|
}
|
|
|
|
func (n *IRCNotifier) SetupPhase(ctx context.Context) {
|
|
if !n.Client.Connected() {
|
|
logging.Info("Connecting to IRC %s", n.Client.Config().Server)
|
|
if ok := n.BackoffCounter.DelayContext(ctx); !ok {
|
|
return
|
|
}
|
|
if err := n.Client.ConnectContext(WithWaitGroup(ctx, &n.sessionWg)); err != nil {
|
|
logging.Error("Could not connect to IRC: %s", err)
|
|
return
|
|
}
|
|
logging.Info("Connected to IRC server, waiting to establish session")
|
|
}
|
|
select {
|
|
case <-n.sessionUpSignal:
|
|
n.sessionUp = true
|
|
n.sessionWg.Add(1)
|
|
n.MaybeGhostNick()
|
|
n.MaybeWaitForNickserv()
|
|
n.channelReconciler.Start(ctx)
|
|
ircConnectedGauge.Set(1)
|
|
case <-n.sessionDownSignal:
|
|
logging.Warn("Receiving a session down before the session is up, this is odd")
|
|
case <-ctx.Done():
|
|
logging.Info("IRC routine asked to terminate")
|
|
}
|
|
}
|
|
|
|
func (n *IRCNotifier) Run(ctx context.Context, stopWg *sync.WaitGroup) {
|
|
defer stopWg.Done()
|
|
|
|
for ctx.Err() != context.Canceled {
|
|
if !n.sessionUp {
|
|
n.SetupPhase(ctx)
|
|
} else {
|
|
n.ConnectedPhase(ctx)
|
|
}
|
|
}
|
|
n.ShutdownPhase()
|
|
}
|