diff --git a/irc/accounts.go b/irc/accounts.go index 63371d6f..95b11663 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -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 } diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 6e0986d3..7ca700e7 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -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 diff --git a/irc/config.go b/irc/config.go index 094a8a1c..6c9cb453 100644 --- a/irc/config.go +++ b/irc/config.go @@ -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 +} diff --git a/irc/errors.go b/irc/errors.go index f5d9a2be..ee283419 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -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`) diff --git a/irc/getters.go b/irc/getters.go index a74a44da..f6d83617 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -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 { diff --git a/irc/handlers.go b/irc/handlers.go index 05b63e2e..fcf09293 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -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 diff --git a/irc/hostserv.go b/irc/hostserv.go index c0462edb..08ffc900 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -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 diff --git a/irc/nickname.go b/irc/nickname.go index 71f7629d..2c26b86b 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -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 diff --git a/irc/nickserv.go b/irc/nickserv.go index 3f4eb9c4..8898e1ac 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -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) { diff --git a/irc/strings.go b/irc/strings.go index 32f3f952..6c8100f6 100644 --- a/irc/strings.go +++ b/irc/strings.go @@ -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: diff --git a/oragono.yaml b/oragono.yaml index 5733d567..661692dc 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -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