mirror of
https://github.com/google/alertmanager-irc-relay.git
synced 2024-11-16 16:19:21 +01:00
a1b44d6ba9
Use newly added ConnectContext function. Note: This is using a yet-to-be-released goirc module version obtained with "go get -u github.com/fluffle/goirc@master" Signed-off-by: Luca Bigliardi <shammash@google.com>
308 lines
8.7 KiB
Go
308 lines
8.7 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"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
irc "github.com/fluffle/goirc/client"
|
|
"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) {
|
|
log.Printf("Received: '%s'", line.Raw)
|
|
}
|
|
|
|
type ChannelState struct {
|
|
Channel IRCChannel
|
|
BackoffCounter Delayer
|
|
}
|
|
|
|
type IRCNotifier struct {
|
|
// Nick stores the nickname specified in the config, because irc.Client
|
|
// might change its copy.
|
|
Nick string
|
|
NickPassword string
|
|
Client *irc.Conn
|
|
AlertMsgs chan AlertMsg
|
|
|
|
ctx context.Context
|
|
stopWg *sync.WaitGroup
|
|
|
|
// 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
|
|
|
|
PreJoinChannels []IRCChannel
|
|
JoinedChannels map[string]ChannelState
|
|
|
|
UsePrivmsg bool
|
|
|
|
NickservDelayWait time.Duration
|
|
BackoffCounter Delayer
|
|
}
|
|
|
|
func NewIRCNotifier(ctx context.Context, stopWg *sync.WaitGroup, config *Config, alertMsgs chan AlertMsg) (*IRCNotifier, error) {
|
|
|
|
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 + "^" }
|
|
|
|
backoffCounter := NewBackoff(
|
|
ircConnectMaxBackoffSecs, ircConnectBackoffResetSecs,
|
|
time.Second)
|
|
|
|
notifier := &IRCNotifier{
|
|
Nick: config.IRCNick,
|
|
NickPassword: config.IRCNickPass,
|
|
Client: irc.Client(ircConfig),
|
|
AlertMsgs: alertMsgs,
|
|
ctx: ctx,
|
|
stopWg: stopWg,
|
|
sessionUpSignal: make(chan bool),
|
|
sessionDownSignal: make(chan bool),
|
|
PreJoinChannels: config.IRCChannels,
|
|
JoinedChannels: make(map[string]ChannelState),
|
|
UsePrivmsg: config.UsePrivmsg,
|
|
NickservDelayWait: nickservWaitSecs * time.Second,
|
|
BackoffCounter: backoffCounter,
|
|
}
|
|
|
|
notifier.Client.HandleFunc(irc.CONNECTED,
|
|
func(*irc.Conn, *irc.Line) {
|
|
log.Printf("Session established")
|
|
notifier.sessionUpSignal <- true
|
|
})
|
|
|
|
notifier.Client.HandleFunc(irc.DISCONNECTED,
|
|
func(*irc.Conn, *irc.Line) {
|
|
log.Printf("Disconnected from IRC")
|
|
notifier.sessionDownSignal <- false
|
|
})
|
|
|
|
notifier.Client.HandleFunc(irc.KICK,
|
|
func(_ *irc.Conn, line *irc.Line) {
|
|
notifier.HandleKick(line.Args[1], line.Args[0])
|
|
})
|
|
|
|
for _, event := range []string{irc.NOTICE, "433"} {
|
|
notifier.Client.HandleFunc(event, loggerHandler)
|
|
}
|
|
|
|
return notifier, nil
|
|
}
|
|
|
|
func (notifier *IRCNotifier) HandleKick(nick string, channel string) {
|
|
if nick != notifier.Client.Me().Nick {
|
|
// received kick info for somebody else
|
|
return
|
|
}
|
|
state, ok := notifier.JoinedChannels[channel]
|
|
if !ok {
|
|
log.Printf("Being kicked out of non-joined channel (%s), ignoring", channel)
|
|
return
|
|
}
|
|
log.Printf("Being kicked out of %s, re-joining", channel)
|
|
go func() {
|
|
if ok := state.BackoffCounter.DelayContext(notifier.ctx); !ok {
|
|
return
|
|
}
|
|
notifier.Client.Join(state.Channel.Name, state.Channel.Password)
|
|
}()
|
|
|
|
}
|
|
|
|
func (notifier *IRCNotifier) CleanupChannels() {
|
|
log.Printf("Deregistering all channels.")
|
|
notifier.JoinedChannels = make(map[string]ChannelState)
|
|
}
|
|
|
|
func (notifier *IRCNotifier) JoinChannel(channel *IRCChannel) {
|
|
if _, joined := notifier.JoinedChannels[channel.Name]; joined {
|
|
return
|
|
}
|
|
log.Printf("Joining %s", channel.Name)
|
|
notifier.Client.Join(channel.Name, channel.Password)
|
|
state := ChannelState{
|
|
Channel: *channel,
|
|
BackoffCounter: NewBackoff(
|
|
ircConnectMaxBackoffSecs, ircConnectBackoffResetSecs,
|
|
time.Second),
|
|
}
|
|
notifier.JoinedChannels[channel.Name] = state
|
|
}
|
|
|
|
func (notifier *IRCNotifier) JoinChannels() {
|
|
for _, channel := range notifier.PreJoinChannels {
|
|
notifier.JoinChannel(&channel)
|
|
}
|
|
}
|
|
|
|
func (notifier *IRCNotifier) MaybeIdentifyNick() {
|
|
if notifier.NickPassword == "" {
|
|
return
|
|
}
|
|
|
|
// Very lazy/optimistic, but this is good enough for my irssi config,
|
|
// so it should work here as well.
|
|
currentNick := notifier.Client.Me().Nick
|
|
if currentNick != notifier.Nick {
|
|
log.Printf("My nick is '%s', sending GHOST to NickServ to get '%s'",
|
|
currentNick, notifier.Nick)
|
|
notifier.Client.Privmsgf("NickServ", "GHOST %s %s", notifier.Nick,
|
|
notifier.NickPassword)
|
|
time.Sleep(notifier.NickservDelayWait)
|
|
|
|
log.Printf("Changing nick to '%s'", notifier.Nick)
|
|
notifier.Client.Nick(notifier.Nick)
|
|
}
|
|
log.Printf("Sending IDENTIFY to NickServ")
|
|
notifier.Client.Privmsgf("NickServ", "IDENTIFY %s", notifier.NickPassword)
|
|
time.Sleep(notifier.NickservDelayWait)
|
|
}
|
|
|
|
func (notifier *IRCNotifier) MaybeSendAlertMsg(alertMsg *AlertMsg) {
|
|
if !notifier.sessionUp {
|
|
log.Printf("Cannot send alert to %s : IRC not connected",
|
|
alertMsg.Channel)
|
|
ircSendMsgErrors.WithLabelValues(alertMsg.Channel, "not_connected").Inc()
|
|
return
|
|
}
|
|
notifier.JoinChannel(&IRCChannel{Name: alertMsg.Channel})
|
|
|
|
if notifier.UsePrivmsg {
|
|
notifier.Client.Privmsg(alertMsg.Channel, alertMsg.Alert)
|
|
} else {
|
|
notifier.Client.Notice(alertMsg.Channel, alertMsg.Alert)
|
|
}
|
|
ircSentMsgs.WithLabelValues(alertMsg.Channel).Inc()
|
|
}
|
|
|
|
func (notifier *IRCNotifier) ShutdownPhase() {
|
|
if notifier.Client.Connected() {
|
|
log.Printf("IRC client connected, quitting")
|
|
notifier.Client.Quit("see ya")
|
|
|
|
if notifier.sessionUp {
|
|
log.Printf("Session is up, wait for IRC disconnect to complete")
|
|
select {
|
|
case <-notifier.sessionDownSignal:
|
|
case <-time.After(notifier.Client.Config().Timeout):
|
|
log.Printf("Timeout while waiting for IRC disconnect to complete, stopping anyway")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (notifier *IRCNotifier) ConnectedPhase() {
|
|
select {
|
|
case alertMsg := <-notifier.AlertMsgs:
|
|
notifier.MaybeSendAlertMsg(&alertMsg)
|
|
case <-notifier.sessionDownSignal:
|
|
notifier.sessionUp = false
|
|
notifier.CleanupChannels()
|
|
notifier.Client.Quit("see ya")
|
|
ircConnectedGauge.Set(0)
|
|
case <-notifier.ctx.Done():
|
|
log.Printf("IRC routine asked to terminate")
|
|
}
|
|
}
|
|
|
|
func (notifier *IRCNotifier) SetupPhase() {
|
|
if !notifier.Client.Connected() {
|
|
log.Printf("Connecting to IRC %s", notifier.Client.Config().Server)
|
|
if ok := notifier.BackoffCounter.DelayContext(notifier.ctx); !ok {
|
|
return
|
|
}
|
|
if err := notifier.Client.ConnectContext(notifier.ctx); err != nil {
|
|
log.Printf("Could not connect to IRC: %s", err)
|
|
return
|
|
}
|
|
log.Printf("Connected to IRC server, waiting to establish session")
|
|
}
|
|
select {
|
|
case <-notifier.sessionUpSignal:
|
|
notifier.sessionUp = true
|
|
notifier.MaybeIdentifyNick()
|
|
notifier.JoinChannels()
|
|
ircConnectedGauge.Set(1)
|
|
case <-notifier.sessionDownSignal:
|
|
log.Printf("Receiving a session down before the session is up, this is odd")
|
|
case <-notifier.ctx.Done():
|
|
log.Printf("IRC routine asked to terminate")
|
|
}
|
|
}
|
|
|
|
func (notifier *IRCNotifier) Run() {
|
|
defer notifier.stopWg.Done()
|
|
|
|
for notifier.ctx.Err() != context.Canceled {
|
|
if !notifier.sessionUp {
|
|
notifier.SetupPhase()
|
|
} else {
|
|
notifier.ConnectedPhase()
|
|
}
|
|
}
|
|
notifier.ShutdownPhase()
|
|
}
|