Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions irc/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,24 +337,28 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
return errAccountAlreadyRegistered
}

config := am.server.AccountConfig()
config := am.server.Config()

// final "is registration allowed" check, probably redundant:
if !(config.Registration.Enabled || callbackNamespace == "admin") {
if !(config.Accounts.Registration.Enabled || callbackNamespace == "admin") {
return errFeatureDisabled
}

// if nick reservation is enabled, you can only register your current nickname
// as an account; this prevents "land-grab" situations where someone else
// registers your nick out from under you and then NS GHOSTs you
// n.b. client is nil during a SAREGISTER:
if config.NickReservation.Enabled && client != nil && client.NickCasefolded() != casefoldedAccount {
// n.b. client is nil during a SAREGISTER
// n.b. if ForceGuestFormat, then there's no concern, because you can't
// register a guest nickname anyway, and the actual registration system
// will prevent any double-register
if client != nil && config.Accounts.NickReservation.Enabled &&
!config.Accounts.NickReservation.ForceGuestFormat &&
client.NickCasefolded() != casefoldedAccount {
return errAccountMustHoldNick
}

// can't register a guest nickname
renamePrefix := strings.ToLower(config.NickReservation.RenamePrefix)
if renamePrefix != "" && strings.HasPrefix(casefoldedAccount, renamePrefix) {
if config.Accounts.NickReservation.guestRegexpFolded.MatchString(casefoldedAccount) {
return errAccountAlreadyRegistered
}

Expand Down Expand Up @@ -382,7 +386,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)

var setOptions *buntdb.SetOptions
ttl := time.Duration(config.Registration.VerifyTimeout)
ttl := time.Duration(config.Accounts.Registration.VerifyTimeout)
if ttl != 0 {
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
}
Expand Down Expand Up @@ -652,7 +656,7 @@ func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount str
}

func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
config := am.server.AccountConfig().Registration.Callbacks.Mailto
config := am.server.Config().Accounts.Registration.Callbacks.Mailto
code = utils.GenerateSecretToken()

subject := config.VerifyMessageSubject
Expand Down Expand Up @@ -811,7 +815,7 @@ func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreser
cfnick, err := CasefoldName(nick)
skeleton, skerr := Skeleton(nick)
// garbage nick, or garbage options, or disabled
nrconfig := am.server.AccountConfig().NickReservation
nrconfig := am.server.Config().Accounts.NickReservation
if err != nil || skerr != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
return errAccountNickReservationFailed
}
Expand Down Expand Up @@ -939,6 +943,10 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
}

func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) (err error) {
// XXX check this now, so we don't allow a redundant login for an always-on client
// even for a brief period. the other potential source of nick-account conflicts
// is from force-nick-equals-account, but those will be caught later by
// fixupNickEqualsAccount and if there is a conflict, they will be logged out.
if client.registered {
if clientAlready := am.server.clients.Get(accountName); clientAlready != nil && clientAlready.AlwaysOn() {
return errNickAccountMismatch
Expand Down Expand Up @@ -1536,7 +1544,7 @@ func (am *AccountManager) VHostListRequests(limit int) (requests []PendingVHostR
func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) {
// if hostserv is disabled in config, then don't grant vhosts
// that were previously approved while it was enabled
if !am.server.AccountConfig().VHosts.Enabled {
if !am.server.Config().Accounts.VHosts.Enabled {
return
}

Expand Down
75 changes: 49 additions & 26 deletions irc/client_lookup_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,9 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
// SetNick sets a client's nickname, validating it against nicknames in use
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) (setNick string, err error) {
config := client.server.Config()
newcfnick, err := CasefoldName(newNick)
if err != nil {
return "", errNicknameInvalid
}
if len(newNick) > config.Limits.NickLen || len(newcfnick) > config.Limits.NickLen {
return "", errNicknameInvalid
}
newSkeleton, err := Skeleton(newNick)
if err != nil {
return "", errNicknameInvalid
}

if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
return "", errNicknameInvalid
}
var newcfnick, newSkeleton string

reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
client.stateMutex.RLock()
account := client.account
accountName := client.accountName
Expand All @@ -141,19 +127,58 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
realname := client.realname
client.stateMutex.RUnlock()

// recompute this (client.alwaysOn is not set for unregistered clients):
alwaysOn := account != "" && persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
// recompute always-on status, because client.alwaysOn is not set for unregistered clients
var alwaysOn, useAccountName bool
if account != "" {
alwaysOn = persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount
}

if useAccountName {
if registered && newNick != accountName && newNick != "" {
return "", errNickAccountMismatch
}
newNick = accountName
newcfnick = account
newSkeleton, err = Skeleton(newNick)
if err != nil {
return "", errNicknameInvalid
}
} else {
newNick = strings.TrimSpace(newNick)
if len(newNick) == 0 {
return "", errNickMissing
}

if alwaysOn && registered {
return "", errCantChangeNick
if account == "" && config.Accounts.NickReservation.ForceGuestFormat {
newNick = strings.Replace(config.Accounts.NickReservation.GuestFormat, "*", newNick, 1)
}

newcfnick, err = CasefoldName(newNick)
if err != nil {
return "", errNicknameInvalid
}
if len(newNick) > config.Limits.NickLen || len(newcfnick) > config.Limits.NickLen {
return "", errNicknameInvalid
}
newSkeleton, err = Skeleton(newNick)
if err != nil {
return "", errNicknameInvalid
}

if restrictedCasefoldedNicks[newcfnick] || restrictedSkeletons[newSkeleton] {
return "", errNicknameInvalid
}

reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
return "", errNicknameReserved
}
}

var bouncerAllowed bool
if config.Accounts.Multiclient.Enabled {
if alwaysOn {
// ignore the pre-reg nick, force a reattach
newNick = accountName
newcfnick = account
if useAccountName {
bouncerAllowed = true
} else {
if config.Accounts.Multiclient.AllowedByDefault && settings.AllowBouncer != MulticlientDisallowedByUser {
Expand Down Expand Up @@ -198,9 +223,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
if skeletonHolder != nil && skeletonHolder != client {
return "", errNicknameInUse
}
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
return "", errNicknameReserved
}

clients.removeInternal(client)
clients.byNick[newcfnick] = client
clients.bySkeleton[newSkeleton] = client
Expand Down
72 changes: 58 additions & 14 deletions irc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package irc

import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
Expand Down Expand Up @@ -242,11 +243,24 @@ type AccountConfig struct {
Duration time.Duration
MaxAttempts int `yaml:"max-attempts"`
} `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation NickReservationConfig `yaml:"nick-reservation"`
Multiclient MulticlientConfig
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
VHosts VHostConfig
SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation struct {
Enabled bool
AdditionalNickLimit int `yaml:"additional-nick-limit"`
Method NickEnforcementMethod
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
RenameTimeout time.Duration `yaml:"rename-timeout"`
// RenamePrefix is the legacy field, GuestFormat is the new version
RenamePrefix string `yaml:"rename-prefix"`
GuestFormat string `yaml:"guest-nickname-format"`
guestRegexp *regexp.Regexp
guestRegexpFolded *regexp.Regexp
ForceGuestFormat bool `yaml:"force-guest-format"`
ForceNickEqualsAccount bool `yaml:"force-nick-equals-account"`
} `yaml:"nick-reservation"`
Multiclient MulticlientConfig
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
VHosts VHostConfig
}

// AccountRegistrationConfig controls account registration.
Expand Down Expand Up @@ -371,15 +385,6 @@ func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err err
return nil
}

type NickReservationConfig struct {
Enabled bool
AdditionalNickLimit int `yaml:"additional-nick-limit"`
Method NickEnforcementMethod
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
RenameTimeout time.Duration `yaml:"rename-timeout"`
RenamePrefix string `yaml:"rename-prefix"`
}

// ChannelRegistrationConfig controls channel registration.
type ChannelRegistrationConfig struct {
Enabled bool
Expand Down Expand Up @@ -883,6 +888,19 @@ func LoadConfig(filename string) (config *Config, err error) {
config.Accounts.Multiclient.AllowedByDefault = true
}

// handle guest format, including the legacy key rename-prefix
if config.Accounts.NickReservation.GuestFormat == "" {
renamePrefix := config.Accounts.NickReservation.RenamePrefix
if renamePrefix == "" {
renamePrefix = "Guest-"
}
config.Accounts.NickReservation.GuestFormat = renamePrefix + "*"
}
config.Accounts.NickReservation.guestRegexp, config.Accounts.NickReservation.guestRegexpFolded, err = compileGuestRegexp(config.Accounts.NickReservation.GuestFormat, config.Server.Casemapping)
if err != nil {
return nil, err
}

var newLogConfigs []logger.LoggingConfig
for _, logConfig := range config.Logging {
// methods
Expand Down Expand Up @@ -1163,3 +1181,29 @@ func (config *Config) Diff(oldConfig *Config) (addedCaps, removedCaps *caps.Set)

return
}

func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) {
starIndex := strings.IndexByte(guestFormat, '*')
if starIndex == -1 {
return nil, nil, errors.New("guest format must contain exactly one *")
}
initial := guestFormat[:starIndex]
final := guestFormat[starIndex+1:]
if strings.IndexByte(final, '*') != -1 {
return nil, nil, errors.New("guest format must contain exactly one *")
}
standard, err = regexp.Compile(fmt.Sprintf("^%s(.*)%s$", initial, final))
if err != nil {
return
}
initialFolded, err := casefoldWithSetting(initial, casemapping)
if err != nil {
return
}
finalFolded, err := casefoldWithSetting(final, casemapping)
if err != nil {
return
}
folded, err = regexp.Compile(fmt.Sprintf("^%s(.*)%s$", initialFolded, finalFolded))
return
}
3 changes: 1 addition & 2 deletions irc/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ var (
errNicknameInvalid = errors.New("invalid nickname")
errNicknameInUse = errors.New("nickname in use")
errNicknameReserved = errors.New("nickname is reserved")
errCantChangeNick = errors.New(`Always-on clients can't change nicknames`)
errNickAccountMismatch = errors.New(`Your nickname doesn't match your account name`)
errNickAccountMismatch = errors.New(`Your nickname must match your account name; try logging out and logging back in with SASL`)
errNoExistingBan = errors.New("Ban does not exist")
errNoSuchChannel = errors.New(`No such channel`)
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
Expand Down
4 changes: 0 additions & 4 deletions irc/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ func (server *Server) ChannelRegistrationEnabled() bool {
return server.Config().Channels.Registration.Enabled
}

func (server *Server) AccountConfig() *AccountConfig {
return &server.Config().Accounts
}

func (server *Server) GetOperator(name string) (oper *Oper) {
name, err := CasefoldName(name)
if err != nil {
Expand Down
10 changes: 7 additions & 3 deletions irc/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
)

// helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234
func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
func parseCallback(spec string, config AccountConfig) (callbackNamespace string, callbackValue string) {
callback := strings.ToLower(spec)
if callback == "*" {
callbackNamespace = "*"
Expand Down Expand Up @@ -127,7 +127,7 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage,
}

// sasl abort
if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
if !config.Accounts.AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
rb.Add(nil, server.name, ERR_SASLABORTED, details.nick, client.t("SASL authentication aborted"))
session.sasl.Clear()
return false
Expand Down Expand Up @@ -237,6 +237,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
msg := authErrorToMessage(server, err)
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
return false
} else if !fixupNickEqualsAccount(client, rb, server.Config()) {
return false
}

sendSuccessfulAccountAuth(client, rb, false, true)
Expand All @@ -245,7 +247,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []

func authErrorToMessage(server *Server, err error) (msg string) {
switch err {
case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch:
case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch:
return err.Error()
default:
// don't expose arbitrary error messages to the user
Expand Down Expand Up @@ -280,6 +282,8 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
msg := authErrorToMessage(server, err)
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
return false
} else if !fixupNickEqualsAccount(client, rb, server.Config()) {
return false
}

sendSuccessfulAccountAuth(client, rb, false, true)
Expand Down
8 changes: 4 additions & 4 deletions irc/hostserv.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func hsNotice(rb *ResponseBuffer, text string) {

// hsNotifyChannel notifies the designated channel of new vhost activity
func hsNotifyChannel(server *Server, message string) {
chname := server.AccountConfig().VHosts.UserRequests.Channel
chname := server.Config().Accounts.VHosts.UserRequests.Channel
channel := server.channels.Get(chname)
if channel == nil {
return
Expand Down Expand Up @@ -280,11 +280,11 @@ func hsStatusHandler(server *Server, client *Client, command string, params []st
}

func validateVhost(server *Server, vhost string, oper bool) error {
ac := server.AccountConfig()
if len(vhost) > ac.VHosts.MaxLength {
config := server.Config()
if len(vhost) > config.Accounts.VHosts.MaxLength {
return errVHostTooLong
}
if !ac.VHosts.ValidRegexp.MatchString(vhost) {
if !config.Accounts.VHosts.ValidRegexp.MatchString(vhost) {
return errVHostBadCharacters
}
return nil
Expand Down
Loading