feat(irc): attempt SASL login with fallback to nickserv (#333)

* IRC: attempt SASL, ignore SASL failure

* update Ergo integration testing config

* refactor(irc): rework auth and join based on events
This commit is contained in:
Shivaram Lingamneni 2022-07-03 13:49:34 -07:00 committed by GitHub
parent 94a3810f57
commit 95471a4cf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 924 additions and 878 deletions

View file

@ -76,6 +76,8 @@ type Handler struct {
connectionErrors []string
failedNickServAttempts int
authenticated bool
}
func NewHandler(log logger.Logger, network domain.IrcNetwork, definitions []*domain.IndexerDefinition, releaseSvc release.Service, notificationSvc notification.Service) *Handler {
@ -90,6 +92,7 @@ func NewHandler(log logger.Logger, network domain.IrcNetwork, definitions []*dom
validAnnouncers: map[string]struct{}{},
validChannels: map[string]struct{}{},
channelHealth: map[string]*channelHealth{},
authenticated: false,
}
// init indexer, announceProcessor
@ -152,6 +155,9 @@ func (h *Handler) Run() error {
User: h.network.NickServ.Account,
RealName: h.network.NickServ.Account,
Password: h.network.Pass,
SASLLogin: h.network.NickServ.Account,
SASLPassword: h.network.NickServ.Password,
SASLOptional: true,
Server: addr,
KeepAlive: 4 * time.Minute,
Timeout: 2 * time.Minute,
@ -176,6 +182,7 @@ func (h *Handler) Run() error {
h.client.AddCallback("PRIVMSG", h.onMessage)
h.client.AddCallback("NOTICE", h.onNotice)
h.client.AddCallback("NICK", h.onNick)
h.client.AddCallback("903", h.handleSASLSuccess)
if err := h.client.Connect(); err != nil {
h.log.Error().Stack().Err(err).Msg("connect error")
@ -219,6 +226,10 @@ func (h *Handler) isOurNick(nick string) bool {
return h.network.NickServ.Account == nick
}
func (h *Handler) isOurCurrentNick(nick string) bool {
return h.client.CurrentNick() == nick
}
func (h *Handler) setConnectionStatus() {
h.m.Lock()
// set connected since now
@ -293,9 +304,10 @@ func (h *Handler) Restart() error {
}
func (h *Handler) onConnect(m ircmsg.Message) {
// 0. Authenticated via SASL - join
// 1. No nickserv, no invite command - join
// 2. Nickserv - join after auth
// 3. nickserv and invite command - join after nickserv
// 2. Nickserv password - join after auth
// 3. nickserv and invite command - send nickserv pass, wait for mode to send invite cmd, then join
// 4. invite command - join
h.resetConnectErrors()
@ -313,9 +325,28 @@ func (h *Handler) onConnect(m ircmsg.Message) {
h.log.Debug().Msgf("connected to: %v", h.network.Name)
time.Sleep(2 * time.Second)
time.Sleep(1 * time.Second)
if h.network.NickServ.Password != "" {
// if already authenticated via SASL then join channels
if h.authenticated {
h.log.Trace().Msg("on connect - already authenticated: join channels")
// check for invite command
if h.network.InviteCommand != "" {
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
h.log.Error().Stack().Err(err).Msgf("error sending connect command %v", h.network.InviteCommand)
return
}
// let's return because MODE will change, and we join when we have the correct mode
return
}
// if authenticated and no invite command lets join
h.JoinChannels()
} else if h.network.NickServ.Password != "" {
h.log.Trace().Msg("on connect not authenticated and password not empty: send nickserv identify")
if err := h.NickServIdentify(h.network.NickServ.Password); err != nil {
h.log.Error().Stack().Err(err).Msg("error nickserv")
return
@ -323,20 +354,21 @@ func (h *Handler) onConnect(m ircmsg.Message) {
// return and wait for NOTICE of nickserv auth
return
}
if h.network.InviteCommand != "" && h.network.NickServ.Password == "" {
} else if h.network.InviteCommand != "" {
h.log.Trace().Msg("on connect invite command not empty: send connect commands")
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
h.log.Error().Stack().Err(err).Msgf("error sending connect command %v", h.network.InviteCommand)
return
}
return
}
} else {
// join channels if no password or no invite command
h.log.Trace().Msg("on connect - no nickserv or invite command: join channels")
h.JoinChannels()
}
}
func (h *Handler) onDisconnect(m ircmsg.Message) {
@ -345,6 +377,7 @@ func (h *Handler) onDisconnect(m ircmsg.Message) {
h.haveDisconnected = true
h.resetConnectionStatus()
h.resetAuthenticated()
// check if we are responsible for disconnect
if !h.manuallyDisconnected {
@ -360,8 +393,14 @@ func (h *Handler) onDisconnect(m ircmsg.Message) {
}
func (h *Handler) onNotice(msg ircmsg.Message) {
if msg.Nick() == "NickServ" {
h.log.Debug().Msgf("NOTICE from nickserv: %v", msg.Params)
switch msg.Nick() {
case "NickServ":
h.handleNickServ(msg)
}
}
func (h *Handler) handleNickServ(msg ircmsg.Message) {
h.log.Trace().Msgf("NOTICE from nickserv: %v", msg.Params)
if contains(msg.Params[1],
"Invalid account credentials",
@ -415,29 +454,14 @@ func (h *Handler) onNotice(msg ircmsg.Message) {
h.failedNickServAttempts++
}
// params: [test-bot You're now logged in as test-bot]
// You're now logged in as test-bot
// Password accepted - you are now recognized.
if contains(msg.Params[1], "you're now logged in as", "password accepted", "you are now recognized") {
h.log.Debug().Msgf("NOTICE nickserv logged in: %v", msg.Params)
h.resetConnectErrors()
h.failedNickServAttempts = 0
// if no invite command, join
if h.network.InviteCommand == "" {
h.JoinChannels()
return
}
// else send connect commands
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
h.log.Error().Stack().Err(err).Msgf("error sending connect command %v", h.network.InviteCommand)
return
}
}
//[test-bot Invalid parameters. For usage, do /msg NickServ HELP IDENTIFY]
// fallback for networks that require both password and nick to NickServ IDENTIFY
// Invalid parameters. For usage, do /msg NickServ HELP IDENTIFY
if contains(msg.Params[1], "invalid parameters", "help identify") {
h.log.Debug().Msgf("NOTICE nickserv invalid: %v", msg.Params)
@ -445,9 +469,25 @@ func (h *Handler) onNotice(msg ircmsg.Message) {
return
}
}
}
// Your nickname is not registered
}
// handleSASLSuccess we get here early so set authenticated before we hit onConnect
func (h *Handler) handleSASLSuccess(msg ircmsg.Message) {
h.setAuthenticated()
}
func (h *Handler) setAuthenticated() {
h.m.Lock()
defer h.m.Unlock()
h.authenticated = true
}
func (h *Handler) resetAuthenticated() {
h.m.Lock()
defer h.m.Unlock()
h.authenticated = false
}
func contains(s string, substr ...string) bool {
@ -464,7 +504,7 @@ func contains(s string, substr ...string) bool {
}
func (h *Handler) onNick(msg ircmsg.Message) {
h.log.Debug().Msgf("NICK event: %v params: %v", msg.Nick(), msg.Params)
h.log.Trace().Msgf("NICK event: %v params: %v", msg.Nick(), msg.Params)
if h.client.CurrentNick() != h.client.PreferredNick() {
h.log.Debug().Msgf("nick miss-match: got %v want %v", h.client.CurrentNick(), h.client.PreferredNick())
@ -565,7 +605,7 @@ func (h *Handler) JoinChannel(channel string, password string) error {
func (h *Handler) handlePart(msg ircmsg.Message) {
if !h.isOurNick(msg.Nick()) {
h.log.Debug().Msgf("MODE OTHER USER: %+v", msg)
h.log.Trace().Msgf("PART other user: %+v", msg)
return
}
@ -573,8 +613,7 @@ func (h *Handler) handlePart(msg ircmsg.Message) {
h.log.Debug().Msgf("PART channel %v", channel)
err := h.client.Part(channel)
if err != nil {
if err := h.client.Part(channel); err != nil {
h.log.Error().Err(err).Msgf("error handling part: %v", channel)
return
}
@ -589,7 +628,7 @@ func (h *Handler) handlePart(msg ircmsg.Message) {
// TODO remove announceProcessor
h.log.Debug().Msgf("Left channel '%v'", channel)
h.log.Debug().Msgf("Left channel %v", channel)
return
}
@ -613,14 +652,14 @@ func (h *Handler) PartChannel(channel string) error {
// TODO remove announceProcessor
h.log.Info().Msgf("Left channel '%v' on network '%v'", channel, h.network.Server)
h.log.Info().Msgf("Left channel: %v", channel)
return nil
}
func (h *Handler) handleJoined(msg ircmsg.Message) {
if !h.isOurNick(msg.Params[0]) {
h.log.Debug().Msgf("OTHER USER JOINED: %+v", msg)
h.log.Trace().Msgf("JOINED other user: %+v", msg)
return
}
@ -705,7 +744,7 @@ func (h *Handler) NickServIdentify(password string) error {
}
func (h *Handler) NickChange(nick string) error {
h.log.Debug().Msgf("Nick change: %v", nick)
h.log.Debug().Msgf("NICK change: %v", nick)
h.client.SetNick(nick)
@ -721,23 +760,34 @@ func (h *Handler) PreferredNick() string {
}
func (h *Handler) handleMode(msg ircmsg.Message) {
h.log.Debug().Msgf("MODE: %+v", msg)
h.log.Trace().Msgf("MODE: %+v", msg)
if !h.isOurNick(msg.Params[0]) {
h.log.Trace().Msgf("MODE OTHER USER: %+v", msg)
// if our nick and user mode +r (Identifies the nick as being Registered (settable by services only)) then return
if h.isOurCurrentNick(msg.Params[0]) && strings.Contains(msg.Params[1], "+r") {
h.setAuthenticated()
h.resetConnectErrors()
h.failedNickServAttempts = 0
// if invite command send
if h.network.InviteCommand != "" {
// send connect commands
if err := h.sendConnectCommands(h.network.InviteCommand); err != nil {
h.log.Error().Stack().Err(err).Msgf("error sending connect command %v", h.network.InviteCommand)
return
}
if h.network.NickServ.Password != "" && !strings.Contains(msg.Params[0], h.client.Nick) || !strings.Contains(msg.Params[1], "+r") {
h.log.Trace().Msgf("MODE: Not correct permission yet: %v", msg.Params)
return
}
time.Sleep(2 * time.Second)
time.Sleep(1 * time.Second)
// join channels
//join channels
h.JoinChannels()
return
}
return
}

View file

@ -1,7 +1,4 @@
# This is the "traditional" or "mainstream" config file for Ergo.
# It tries to replicate the behavior of other ircds, at the cost of not
# taking full advantage of Ergo's features. This config is suitable for use
# in IRCv3 conformance testing.
# Ergo config for integration testing, based on Ergo's `default.yaml`
# network configuration
network:
@ -15,10 +12,7 @@ server:
# addresses to listen on
listeners:
# This version of the config provides a public plaintext listener on
# port 6667 for testing and compatibility with legacy applications.
# We recommend disabling this listener in a production setting
# and replacing it with loopback-only listeners (see default.yaml):
# XXX enable public plaintext listener to simplify Docker configuration:
":6667":
# The standard SSL/TLS port for IRC is 6697. This will listen on all interfaces:
@ -32,8 +26,8 @@ server:
# always send a PROXY protocol header ahead of the connection. See the
# manual ("Reverse proxies") for more details.
proxy: false
# optionally set the minimum TLS version (defaults to 1.0):
# min-tls-version: 1.2
# set the minimum TLS version:
min-tls-version: 1.2
# Example of a Unix domain socket for proxying:
# "/tmp/ergo_sock":
@ -121,22 +115,22 @@ server:
enforce-utf8: true
# whether to look up user hostnames with reverse DNS. there are 3 possibilities:
# 1. [enabled here] lookup-hostnames enabled, IP cloaking disabled; users will see each other's hostnames
# 1. lookup-hostnames enabled, IP cloaking disabled; users will see each other's hostnames
# 2. lookup-hostnames disabled, IP cloaking disabled; users will see each other's numeric IPs
# 3. IP cloaking enabled; users will see cloaked hostnames
lookup-hostnames: true
# 3. [the default] IP cloaking enabled; users will see cloaked hostnames
lookup-hostnames: false
# whether to confirm hostname lookups using "forward-confirmed reverse DNS", i.e., for
# any hostname returned from reverse DNS, resolve it back to an IP address and reject it
# unless it matches the connecting IP
forward-confirm-hostnames: true
# use ident protocol to get usernames
check-ident: true
check-ident: false
# ignore the supplied user/ident string from the USER command, always setting user/ident
# to the following literal value; this can potentially reduce confusion and simplify bans.
# the value must begin with a '~' character. comment out / omit to disable:
#coerce-ident: '~u'
coerce-ident: '~u'
# password to login to the server, generated using `ergo genpasswd`:
#password: "$2a$04$0123456789abcdef0123456789abcdef0123456789abcdef01234"
@ -180,7 +174,7 @@ server:
certfp: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
# password the gateway uses to connect, made with `ergo genpasswd`
password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde" # test
password: "$2a$04$abcdef0123456789abcdef0123456789abcdef0123456789abcde"
# IPs/CIDRs that can use this webirc command
# you should also add these addresses to the connection limits and throttling exemption lists
@ -213,7 +207,7 @@ server:
# allow-truncation to false, in which case Ergo will reject the message
# and return an error to the client. (note that this option defaults to true
# when unset.)
allow-truncation: true
allow-truncation: false
# IP-based DoS protection
ip-limits:
@ -284,9 +278,10 @@ server:
# DNS, users see fake domain names like pwbs2ui4377257x8.irc. These names are
# generated deterministically from the underlying IP address, but if the underlying
# IP is not already known, it is infeasible to recover it from the cloaked name.
# If you disable this, you should probably enable lookup-hostnames in its place.
ip-cloaking:
# whether to enable IP cloaking
enabled: false
enabled: true
# whether to use these cloak settings (specifically, `netname` and `num-bits`)
# to produce unique hostnames for always-on clients. you can enable this even if
@ -418,7 +413,7 @@ accounts:
# enable login to accounts via the PASS command, e.g., PASS account:password
# this is useful for compatibility with old clients that don't support SASL
login-via-pass-command: false
login-via-pass-command: true
# require-sasl controls whether clients are required to have accounts
# (and sign into them using SASL) to connect to the server
@ -442,7 +437,7 @@ accounts:
# how many nicknames, in addition to the account name, can be reserved?
# (note that additional nicks are unusable under force-nick-equals-account
# or if the client is always-on)
additional-nick-limit: 2
additional-nick-limit: 0
# method describes how nickname reservation is handled
# strict: users must already be logged in to their account (via
@ -450,11 +445,11 @@ accounts:
# in order to use their reserved nickname(s)
# optional: no enforcement by default, but allow users to opt in to
# the enforcement level of their choice
method: optional
method: strict
# allow users to set their own nickname enforcement status, e.g.,
# to opt out of strict enforcement
allow-custom-enforcement: true
allow-custom-enforcement: false
# format for guest nicknames:
# 1. these nicknames cannot be registered or reserved
@ -475,7 +470,7 @@ accounts:
# account name as their nickname. when combined with strict nickname
# enforcement, this lets users treat nicknames and account names
# as equivalent for the purpose of ban/invite/exception lists.
force-nick-equals-account: false
force-nick-equals-account: true
# parallel setting to force-nick-equals-account: if true, this forbids
# anonymous users (i.e., users not logged into an account) to change their
@ -495,12 +490,12 @@ accounts:
# if this is disabled, clients have to opt in to bouncer functionality
# using nickserv or the cap system. if it's enabled, they can opt out
# via nickserv
allowed-by-default: false
allowed-by-default: true
# whether to allow clients that remain on the server even
# when they have no active connections. The possible values are:
# "disabled", "opt-in", "opt-out", or "mandatory".
always-on: "disabled"
always-on: "opt-in"
# whether to mark always-on clients away when they have no active connections:
auto-away: "opt-in"
@ -526,7 +521,7 @@ accounts:
# if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies)
# see /QUOTE HELP umodes for more user modes
# default-user-modes: +i
default-user-modes: +i
# pluggable authentication mechanism, via subprocess invocation
# see the manual for details on how to write an authentication plugin script
@ -548,9 +543,10 @@ accounts:
# channel options
channels:
# modes that are set when new channels are created
# +n is no-external-messages and +t is op-only-topic
# +n is no-external-messages, +t is op-only-topic,
# +C is no CTCPs (besides ACTION)
# see /QUOTE HELP cmodes for more channel modes
default-modes: +nt
default-modes: +ntC
# how many channels can a client be in at once?
max-channels-per-client: 100
@ -631,13 +627,13 @@ opers:
# traditionally, operator status is visible to unprivileged users in
# WHO and WHOIS responses. this can be disabled with 'hidden'.
hidden: false
hidden: true
# custom whois line (if `hidden` is enabled, visible only to other operators)
whois-line: is the server administrator
# custom hostname (ignored if `hidden` is enabled)
vhost: "staff"
#vhost: "staff"
# modes are modes to auto-set upon opering-up. uncomment this to automatically
# enable snomasks ("server notification masks" that alert you to server events;
@ -842,19 +838,19 @@ roleplay:
# in effect, the server can sign a token attesting that the client is present on
# the server, is a member of a particular channel, etc.
extjwt:
# # default service config (for `EXTJWT #channel`).
# # expiration time for the token:
# expiration: 45s
# # you can configure tokens to be signed either with HMAC and a symmetric secret:
# secret: "65PHvk0K1_sM-raTsCEhatVkER_QD8a0zVV8gG2EWcI"
# # or with an RSA private key:
# #rsa-private-key-file: "extjwt.pem"
# # default service config (for `EXTJWT #channel`).
# # expiration time for the token:
# expiration: 45s
# # you can configure tokens to be signed either with HMAC and a symmetric secret:
# secret: "65PHvk0K1_sM-raTsCEhatVkER_QD8a0zVV8gG2EWcI"
# # or with an RSA private key:
# #rsa-private-key-file: "extjwt.pem"
# # named services (for `EXTJWT #channel service_name`):
# services:
# "jitsi":
# expiration: 30s
# secret: "qmamLKDuOzIzlO8XqsGGewei_At11lewh6jtKfSTbkg"
# # named services (for `EXTJWT #channel service_name`):
# services:
# "jitsi":
# expiration: 30s
# secret: "qmamLKDuOzIzlO8XqsGGewei_At11lewh6jtKfSTbkg"
# history message storage: this is used by CHATHISTORY, HISTORY, znc.in/playback,
# various autoreplay features, and the resume extension