mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +00:00
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:
parent
94a3810f57
commit
95471a4cf7
2 changed files with 924 additions and 878 deletions
|
@ -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,19 +760,27 @@ 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
|
||||
h.JoinChannels()
|
||||
|
@ -741,6 +788,9 @@ func (h *Handler) handleMode(msg ircmsg.Message) {
|
|||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// check if announcer is one from the list in the definition
|
||||
func (h *Handler) isValidAnnouncer(nick string) bool {
|
||||
_, ok := h.validAnnouncers[nick]
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue