diff --git a/irc/accounts.go b/irc/accounts.go index a9152555..4a6298eb 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -33,6 +33,7 @@ const ( keyAccountEnforcement = "account.customenforcement %s" keyAccountVHost = "account.vhost %s" keyCertToAccount = "account.creds.certfp %s" + keyAccountChannels = "account.channels %s" keyVHostQueueAcctToId = "vhostQueue %s" vhostRequestIdx = "vhostQueue" @@ -856,9 +857,25 @@ func (am *AccountManager) Unregister(account string) error { nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount) + channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount) var clients []*Client + var registeredChannels []string + // on our way out, unregister all the account's channels and delete them from the db + defer func() { + for _, channelName := range registeredChannels { + info := am.server.channelRegistry.LoadChannel(channelName) + if info != nil && info.Founder == casefoldedAccount { + am.server.channelRegistry.Delete(channelName, *info) + } + channel := am.server.channels.Get(channelName) + if channel != nil { + channel.SetUnregistered(casefoldedAccount) + } + } + }() + var credText string var rawNicks string @@ -866,6 +883,7 @@ func (am *AccountManager) Unregister(account string) error { defer am.serialCacheUpdateMutex.Unlock() var accountName string + var channelsStr string am.server.store.Update(func(tx *buntdb.Tx) error { tx.Delete(accountKey) accountName, _ = tx.Get(accountNameKey) @@ -879,6 +897,9 @@ func (am *AccountManager) Unregister(account string) error { credText, err = tx.Get(credentialsKey) tx.Delete(credentialsKey) tx.Delete(vhostKey) + channelsStr, _ = tx.Get(channelsKey) + tx.Delete(channelsKey) + _, err := tx.Delete(vhostQueueKey) am.decrementVHostQueueCount(casefoldedAccount, err) return nil @@ -899,6 +920,7 @@ func (am *AccountManager) Unregister(account string) error { skeleton, _ := Skeleton(accountName) additionalNicks := unmarshalReservedNicks(rawNicks) + registeredChannels = unmarshalRegisteredChannels(channelsStr) am.Lock() defer am.Unlock() @@ -925,9 +947,32 @@ func (am *AccountManager) Unregister(account string) error { if err != nil { return errAccountDoesNotExist } + return nil } +func unmarshalRegisteredChannels(channelsStr string) (result []string) { + if channelsStr != "" { + result = strings.Split(channelsStr, ",") + } + return +} + +func (am *AccountManager) ChannelsForAccount(account string) (channels []string) { + cfaccount, err := CasefoldName(account) + if err != nil { + return + } + + var channelStr string + key := fmt.Sprintf(keyAccountChannels, cfaccount) + am.server.store.View(func(tx *buntdb.Tx) error { + channelStr, _ = tx.Get(key) + return nil + }) + return unmarshalRegisteredChannels(channelStr) +} + func (am *AccountManager) AuthenticateByCertFP(client *Client) error { if client.certfp == "" { return errAccountInvalidCredentials diff --git a/irc/channel.go b/irc/channel.go index 0e07d96a..05194ddc 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -165,10 +165,13 @@ func (channel *Channel) SetRegistered(founder string) error { } // SetUnregistered deletes the channel's registration information. -func (channel *Channel) SetUnregistered() { +func (channel *Channel) SetUnregistered(expectedFounder string) { channel.stateMutex.Lock() defer channel.stateMutex.Unlock() + if channel.registeredFounder != expectedFounder { + return + } channel.registeredFounder = "" var zeroTime time.Time channel.registeredTime = zeroTime diff --git a/irc/channelreg.go b/irc/channelreg.go index 723cfdaa..7b05bccc 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -254,14 +254,43 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist for _, keyFmt := range channelKeyStrings { tx.Delete(fmt.Sprintf(keyFmt, key)) } + + // remove this channel from the client's list of registered channels + channelsKey := fmt.Sprintf(keyAccountChannels, info.Founder) + channelsStr, err := tx.Get(channelsKey) + if err == buntdb.ErrNotFound { + return + } + registeredChannels := unmarshalRegisteredChannels(channelsStr) + var nowRegisteredChannels []string + for _, channel := range registeredChannels { + if channel != key { + nowRegisteredChannels = append(nowRegisteredChannels, channel) + } + } + tx.Set(channelsKey, strings.Join(nowRegisteredChannels, ","), nil) } } } // saveChannel saves a channel to the store. func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel, includeFlags uint) { + // maintain the mapping of account -> registered channels + chanExistsKey := fmt.Sprintf(keyChannelExists, channelKey) + _, existsErr := tx.Get(chanExistsKey) + if existsErr == buntdb.ErrNotFound { + // this is a new registration, need to update account-to-channels + accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder) + alreadyChannels, _ := tx.Get(accountChannelsKey) + newChannels := channelKey // this is the casefolded channel name + if alreadyChannels != "" { + newChannels = fmt.Sprintf("%s,%s", alreadyChannels, newChannels) + } + tx.Set(accountChannelsKey, newChannels, nil) + } + if includeFlags&IncludeInitial != 0 { - tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) + tx.Set(chanExistsKey, "1", nil) tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil) tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil) diff --git a/irc/chanserv.go b/irc/chanserv.go index d91022cf..c72059b6 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -224,8 +224,15 @@ func csRegisterHandler(server *Server, client *Client, command string, params [] return } + account := client.Account() + channelsAlreadyRegistered := server.accounts.ChannelsForAccount(account) + if server.Config().Channels.Registration.MaxChannelsPerAccount <= len(channelsAlreadyRegistered) { + csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER")) + return + } + // this provides the synchronization that allows exactly one registration of the channel: - err = channelInfo.SetRegistered(client.Account()) + err = channelInfo.SetRegistered(account) if err != nil { csNotice(rb, err.Error()) return @@ -270,11 +277,13 @@ func csUnregisterHandler(server *Server, client *Client, command string, params return } - hasPrivs := client.HasRoleCapabs("chanreg") - if !hasPrivs { - founder := channel.Founder() - hasPrivs = founder != "" && founder == client.Account() + founder := channel.Founder() + if founder == "" { + csNotice(rb, client.t("That channel is not registered")) + return } + + hasPrivs := client.HasRoleCapabs("chanreg") || founder == client.Account() if !hasPrivs { csNotice(rb, client.t("Insufficient privileges")) return @@ -288,8 +297,8 @@ func csUnregisterHandler(server *Server, client *Client, command string, params return } - channel.SetUnregistered() - go server.channelRegistry.Delete(channelKey, info) + channel.SetUnregistered(founder) + server.channelRegistry.Delete(channelKey, info) csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey)) } diff --git a/irc/config.go b/irc/config.go index 01a18976..112a5387 100644 --- a/irc/config.go +++ b/irc/config.go @@ -181,7 +181,8 @@ type NickReservationConfig struct { // ChannelRegistrationConfig controls channel registration. type ChannelRegistrationConfig struct { - Enabled bool + Enabled bool + MaxChannelsPerAccount int `yaml:"max-channels-per-account"` } // OperClassConfig defines a specific operator class. @@ -293,9 +294,10 @@ type Config struct { Accounts AccountConfig Channels struct { - DefaultModes *string `yaml:"default-modes"` - defaultModes modes.Modes - Registration ChannelRegistrationConfig + DefaultModes *string `yaml:"default-modes"` + defaultModes modes.Modes + MaxChannelsPerClient int `yaml:"max-channels-per-client"` + Registration ChannelRegistrationConfig } OperClasses map[string]*OperClassConfig `yaml:"oper-classes"` @@ -789,6 +791,13 @@ func LoadConfig(filename string) (config *Config, err error) { config.Accounts.Registration.BcryptCost = passwd.DefaultCost } + if config.Channels.MaxChannelsPerClient == 0 { + config.Channels.MaxChannelsPerClient = 100 + } + if config.Channels.Registration.MaxChannelsPerAccount == 0 { + config.Channels.Registration.MaxChannelsPerAccount = 15 + } + // in the current implementation, we disable history by creating a history buffer // with zero capacity. but the `enabled` config option MUST be respected regardless // of this detail diff --git a/irc/database.go b/irc/database.go index 27974668..c85cebc0 100644 --- a/irc/database.go +++ b/irc/database.go @@ -22,7 +22,7 @@ const ( // 'version' of the database schema keySchemaVersion = "db.version" // latest schema of the db - latestDbSchema = "4" + latestDbSchema = "5" ) type SchemaChanger func(*Config, *buntdb.Tx) error @@ -390,6 +390,25 @@ func schemaChangeV3ToV4(config *Config, tx *buntdb.Tx) error { return nil } +// create new key tracking channels that belong to an account +func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error { + founderToChannels := make(map[string][]string) + prefix := "channel.founder " + tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + channel := strings.TrimPrefix(key, prefix) + founderToChannels[value] = append(founderToChannels[value], channel) + return true + }) + + for founder, channels := range founderToChannels { + tx.Set(fmt.Sprintf("account.channels %s", founder), strings.Join(channels, ","), nil) + } + return nil +} + func init() { allChanges := []SchemaChange{ { @@ -407,6 +426,11 @@ func init() { TargetVersion: "4", Changer: schemaChangeV3ToV4, }, + { + InitialVersion: "4", + TargetVersion: "5", + Changer: schemaChangeV4ToV5, + }, } // build the index diff --git a/irc/getters.go b/irc/getters.go index 7eb3e546..a1557d62 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -200,6 +200,12 @@ func (client *Client) Channels() (result []*Channel) { return } +func (client *Client) NumChannels() int { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + return len(client.channels) +} + func (client *Client) WhoWas() (result WhoWas) { return client.Details().WhoWas } diff --git a/irc/handlers.go b/irc/handlers.go index ae96f78f..81f0d791 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1159,7 +1159,13 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp keys = strings.Split(msg.Params[1], ",") } + config := server.Config() + oper := client.Oper() for i, name := range channels { + if config.Channels.MaxChannelsPerClient <= client.NumChannels() && oper == nil { + rb.Add(nil, server.name, ERR_TOOMANYCHANNELS, client.Nick(), name, client.t("You have joined too many channels")) + return false + } var key string if len(keys) > i { key = keys[i] diff --git a/irc/nickserv.go b/irc/nickserv.go index d087fb8b..2a2abcfb 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -327,6 +327,9 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri for _, nick := range account.AdditionalNicks { nsNotice(rb, fmt.Sprintf(client.t("Additional grouped nick: %s"), nick)) } + for _, channel := range server.accounts.ChannelsForAccount(accountName) { + nsNotice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel)) + } } func nsRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { diff --git a/oragono.yaml b/oragono.yaml index 1c27065b..8181f729 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -275,11 +275,17 @@ channels: # see /QUOTE HELP cmodes for more channel modes default-modes: +nt + # how many channels can a client be in at once? + max-channels-per-client: 100 + # channel registration - requires an account registration: # can users register new channels? enabled: true + # how many channels can each account register? + max-channels-per-account: 15 + # operator classes oper-classes: # local operator