This commit is contained in:
Shivaram Lingamneni 2020-03-16 07:54:50 -04:00
parent ee63cd135b
commit 26fd3e69a8
11 changed files with 208 additions and 95 deletions

View File

@ -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 EnforceGuestFormat, 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.EnforceGuestFormat &&
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
}
@ -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}
}
@ -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
@ -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
}
@ -1536,7 +1540,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
}

View File

@ -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
@ -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.EnforceAccountName
}
if alwaysOn && registered {
return "", errCantChangeNick
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 account == "" && config.Accounts.NickReservation.EnforceGuestFormat {
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 {
@ -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

View File

@ -7,6 +7,7 @@ package irc
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
@ -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
EnforceGuestFormat bool `yaml:"enforce-guest-format"`
EnforceAccountName bool `yaml:"enforce-account-name"`
} `yaml:"nick-reservation"`
Multiclient MulticlientConfig
Bouncer *MulticlientConfig // # handle old name for 'multiclient'
VHosts VHostConfig
}
// AccountRegistrationConfig controls account registration.
@ -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
@ -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
@ -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
}

View File

@ -41,8 +41,8 @@ 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`)
errCantChangeNick = errors.New(`You must use your account name as your nickname`)
errNickAccountMismatch = errors.New(`Your nickname must match your account name`)
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`)

View File

@ -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 {

View File

@ -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 = "*"
@ -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

View File

@ -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
@ -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

View File

@ -6,7 +6,6 @@ package irc
import (
"crypto/rand"
"encoding/hex"
"fmt"
"strings"
@ -27,22 +26,15 @@ var (
)
// returns whether the change succeeded or failed
func performNickChange(server *Server, client *Client, target *Client, session *Session, newnick string, rb *ResponseBuffer) bool {
nickname := strings.TrimSpace(newnick)
func performNickChange(server *Server, client *Client, target *Client, session *Session, nickname string, rb *ResponseBuffer) bool {
currentNick := client.Nick()
if len(nickname) < 1 {
rb.Add(nil, server.name, ERR_NONICKNAMEGIVEN, currentNick, client.t("No nickname given"))
return false
}
if target.Nick() == nickname {
details := target.Details()
if details.nick == nickname {
return true
}
hadNick := details.nick != "*"
origNickMask := details.nickMask
hadNick := target.HasNick()
origNickMask := target.NickMaskString()
details := target.Details()
assignedNickname, err := client.server.clients.SetNick(target, session, nickname)
if err == errNicknameInUse {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
@ -50,8 +42,12 @@ func performNickChange(server *Server, client *Client, target *Client, session *
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is reserved by a different account"))
} else if err == errNicknameInvalid {
rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, utils.SafeErrorParam(nickname), client.t("Erroneous nickname"))
} else if err == errCantChangeNick {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, utils.SafeErrorParam(nickname), client.t(err.Error()))
} else if err == errNickAccountMismatch {
// this used to use ERR_NICKNAMEINUSE, but it displayed poorly in some clients;
// ERR_UNKNOWNERROR at least has a better chance of displaying our error text
rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", client.t(err.Error()))
} else if err == errNickMissing {
rb.Add(nil, server.name, ERR_NONICKNAMEGIVEN, currentNick, client.t("No nickname given"))
} else if err != nil {
rb.Add(nil, server.name, ERR_UNKNOWNERROR, currentNick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
}
@ -96,13 +92,10 @@ func performNickChange(server *Server, client *Client, target *Client, session *
}
func (server *Server) RandomlyRename(client *Client) {
prefix := server.AccountConfig().NickReservation.RenamePrefix
if prefix == "" {
prefix = "Guest-"
}
format := server.Config().Accounts.NickReservation.GuestFormat
buf := make([]byte, 8)
rand.Read(buf)
nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf))
nick := strings.Replace(format, "*", utils.B32Encoder.EncodeToString(buf), -1)
sessions := client.Sessions()
if len(sessions) == 0 {
return

View File

@ -610,6 +610,23 @@ func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
return true
}
// if enforce-account-name is set, account name and nickname must be equal,
// so we need to re-NICK automatically on every login event (IDENTIFY,
// VERIFY, and a REGISTER that auto-verifies). if we can't get the nick
// then we log them out (they will be able to reattach with SASL)
func nsFixNickname(client *Client, rb *ResponseBuffer, config *Config) (success bool) {
if !config.Accounts.NickReservation.EnforceAccountName {
return true
}
// don't need to supply a nickname, SetNick will use the account name
if !performNickChange(client.server, client, client, rb.session, "", rb) {
client.server.accounts.Logout(client)
nsNotice(rb, client.t("A client is already using that account; try logging out and logging back in with SASL"))
return false
}
return true
}
func nsIdentifyHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
if client.LoggedIntoAccount() {
nsNotice(rb, client.t("You're already logged into an account"))
@ -649,11 +666,16 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
loginSuccessful = (err == nil)
}
if loginSuccessful {
if !nsFixNickname(client, rb, server.Config()) {
loginSuccessful = false
err = errNickAccountMismatch
}
}
if loginSuccessful {
sendSuccessfulAccountAuth(client, rb, true, true)
} else if err == errNickAccountMismatch {
nsNotice(rb, client.t("That account is set to always-on; try logging out and logging back in with SASL"))
} else {
} else if err != errNickAccountMismatch {
nsNotice(rb, client.t("Could not login with your TLS certificate or supplied username/password"))
}
}
@ -667,7 +689,7 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri
var accountName string
if len(params) > 0 {
nick := params[0]
if server.AccountConfig().NickReservation.Enabled {
if server.Config().Accounts.NickReservation.Enabled {
accountName = server.accounts.NickToAccount(nick)
if accountName == "" {
nsNotice(rb, client.t("That nickname is not registered"))
@ -704,7 +726,6 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri
func nsRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
details := client.Details()
account := details.nick
passphrase := params[0]
var email string
if 1 < len(params) {
@ -730,10 +751,20 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
return
}
config := server.AccountConfig()
config := server.Config()
account := details.nick
if config.Accounts.NickReservation.EnforceGuestFormat {
matches := config.Accounts.NickReservation.guestRegexp.FindStringSubmatch(account)
if matches == nil || len(matches) < 2 {
nsNotice(rb, client.t("Erroneous nickname"))
return
}
account = matches[1]
}
var callbackNamespace, callbackValue string
noneCallbackAllowed := false
for _, callback := range config.Registration.EnabledCallbacks {
for _, callback := range config.Accounts.Registration.EnabledCallbacks {
if callback == "*" {
noneCallbackAllowed = true
}
@ -744,7 +775,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
if noneCallbackAllowed {
callbackNamespace = "*"
} else {
callbackNamespace, callbackValue = parseCallback(email, config)
callbackNamespace, callbackValue = parseCallback(email, config.Accounts)
if callbackNamespace == "" || callbackValue == "" {
nsNotice(rb, client.t("Registration requires a valid e-mail address"))
return
@ -755,7 +786,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
if err == nil {
if callbackNamespace == "*" {
err = server.accounts.Verify(client, account, "")
if err == nil {
if err == nil && nsFixNickname(client, rb, config) {
sendSuccessfulRegResponse(client, rb, true)
}
} else {
@ -861,7 +892,9 @@ func nsVerifyHandler(server *Server, client *Client, command string, params []st
return
}
sendSuccessfulRegResponse(client, rb, true)
if nsFixNickname(client, rb, server.Config()) {
sendSuccessfulRegResponse(client, rb, true)
}
}
func nsPasswdHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {

View File

@ -76,7 +76,11 @@ func iterateFolding(profile *precis.Profile, oldStr string) (str string, err err
// Casefold returns a casefolded string, without doing any name or channel character checks.
func Casefold(str string) (string, error) {
switch globalCasemappingSetting {
return casefoldWithSetting(str, globalCasemappingSetting)
}
func casefoldWithSetting(str string, setting Casemapping) (string, error) {
switch setting {
default:
return iterateFolding(precis.UsernameCaseMapped, str)
case CasemappingASCII:

View File

@ -343,8 +343,24 @@ accounts:
# rename-timeout - this is how long users have 'til they're renamed
rename-timeout: 30s
# rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
rename-prefix: Guest-
# format for guest nicknames:
# 1. these nicknames cannot be registered or reserved
# 2. if a client is automatically renamed by the server,
# this is the template that will be used (e.g., Guest-nccj6rgmt97cg)
# 3. if enforce-guest-format (see below) is enabled, clients without
# a registered account will have this template applied to their
# nicknames (e.g., 'katie' will become 'Guest-katie')
guest-nickname-format: "Guest-*"
# when enabled, forces users not logged into an account to use
# a nickname matching the guest template:
enforce-guest-format: false
# when enabled, forces users logged into an account to use the
# 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.
enforce-account-name: false
# multiclient controls whether oragono allows multiple connections to
# attach to the same client/nickname identity; this is part of the