From ff7bbc4a9c60afb7bc1d30d6df56e2d330a6e7c2 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 6 Feb 2019 04:32:04 -0500 Subject: [PATCH 1/5] track channel registrations per account * limit the total number of registrations per account * when an account is unregistered, unregister all its channels --- irc/accounts.go | 35 +++++++++++++++++++++++++++++++++++ irc/channelreg.go | 42 +++++++++++++++++++++++++++++++++++++++++- irc/chanserv.go | 7 +++++++ irc/config.go | 7 ++++++- irc/database.go | 26 +++++++++++++++++++++++++- irc/nickserv.go | 3 +++ oragono.yaml | 3 +++ 7 files changed, 120 insertions(+), 3 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index 061caeec..ccf3a426 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" @@ -836,9 +837,15 @@ 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 + defer func() { + am.server.channelRegistry.deleteByAccount(casefoldedAccount, registeredChannels) + }() + var credText string var rawNicks string @@ -846,6 +853,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) @@ -859,6 +867,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 @@ -879,6 +890,7 @@ func (am *AccountManager) Unregister(account string) error { skeleton, _ := Skeleton(accountName) additionalNicks := unmarshalReservedNicks(rawNicks) + registeredChannels = unmarshalRegisteredChannels(channelsStr) am.Lock() defer am.Unlock() @@ -899,9 +911,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/channelreg.go b/irc/channelreg.go index 723cfdaa..e9792335 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -215,6 +215,17 @@ func (reg *ChannelRegistry) Delete(casefoldedName string, info RegisteredChannel }) } +// deleteByAccount is a helper to delete all channel registrations corresponding to a user account. +func (reg *ChannelRegistry) deleteByAccount(cfaccount string, cfchannels []string) { + for _, cfchannel := range cfchannels { + info := reg.LoadChannel(cfchannel) + if info == nil || info.Founder != cfaccount { + continue + } + reg.Delete(cfchannel, *info) + } +} + // Rename handles the persistence part of a channel rename: the channel is // persisted under its new name, and the old name is cleaned up if necessary. func (reg *ChannelRegistry) Rename(channel *Channel, casefoldedOldName string) { @@ -254,14 +265,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 9b502161..6ba17931 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -224,6 +224,13 @@ 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()) if err != nil { diff --git a/irc/config.go b/irc/config.go index 01a18976..81f70d1b 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. @@ -789,6 +790,10 @@ func LoadConfig(filename string) (config *Config, err error) { config.Accounts.Registration.BcryptCost = passwd.DefaultCost } + if config.Channels.Registration.MaxChannelsPerAccount == 0 { + config.Channels.Registration.MaxChannelsPerAccount = 10 + } + // 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/nickserv.go b/irc/nickserv.go index 8ebe0b52..ddb3923b 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -318,6 +318,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 a63e9475..1f49900b 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -280,6 +280,9 @@ channels: # can users register new channels? enabled: true + # how many channels can each account register? + max-channels-per-account: 10 + # operator classes oper-classes: # local operator From eff2571096f24ff5083897cb054a22c898beab69 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 6 Feb 2019 04:55:05 -0500 Subject: [PATCH 2/5] limit the number of channels a client can join --- irc/config.go | 10 +++++++--- irc/getters.go | 6 ++++++ irc/handlers.go | 6 ++++++ oragono.yaml | 3 +++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/irc/config.go b/irc/config.go index 81f70d1b..1e4ae0a1 100644 --- a/irc/config.go +++ b/irc/config.go @@ -294,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"` @@ -790,6 +791,9 @@ 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 = 10 } diff --git a/irc/getters.go b/irc/getters.go index ff3f8c85..72ef9c89 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -184,6 +184,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 45112aad..394d2676 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1148,7 +1148,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_UNKNOWNERROR, client.Nick(), name, client.t("You have joined too many channels already")) + return false + } var key string if len(keys) > i { key = keys[i] diff --git a/oragono.yaml b/oragono.yaml index 1f49900b..6f23e243 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -275,6 +275,9 @@ 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? From 2910eda73717c2b642478bf2530b496decf74d3f Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 6 Feb 2019 14:28:17 -0500 Subject: [PATCH 3/5] fix a nit --- irc/chanserv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irc/chanserv.go b/irc/chanserv.go index 6ba17931..333b3975 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -232,7 +232,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params [] } // 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 From 370255bec1888ecfe30a2524ae652da62b1cfb50 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 6 Feb 2019 15:47:20 -0500 Subject: [PATCH 4/5] review fixes --- irc/config.go | 2 +- irc/handlers.go | 2 +- oragono.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/irc/config.go b/irc/config.go index 1e4ae0a1..112a5387 100644 --- a/irc/config.go +++ b/irc/config.go @@ -795,7 +795,7 @@ func LoadConfig(filename string) (config *Config, err error) { config.Channels.MaxChannelsPerClient = 100 } if config.Channels.Registration.MaxChannelsPerAccount == 0 { - config.Channels.Registration.MaxChannelsPerAccount = 10 + config.Channels.Registration.MaxChannelsPerAccount = 15 } // in the current implementation, we disable history by creating a history buffer diff --git a/irc/handlers.go b/irc/handlers.go index 394d2676..cceae379 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1152,7 +1152,7 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp oper := client.Oper() for i, name := range channels { if config.Channels.MaxChannelsPerClient <= client.NumChannels() && oper == nil { - rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), name, client.t("You have joined too many channels already")) + rb.Add(nil, server.name, ERR_TOOMANYCHANNELS, client.Nick(), name, client.t("You have joined too many channels")) return false } var key string diff --git a/oragono.yaml b/oragono.yaml index 6f23e243..8009571b 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -284,7 +284,7 @@ channels: enabled: true # how many channels can each account register? - max-channels-per-account: 10 + max-channels-per-account: 15 # operator classes oper-classes: From e4c9351254637a1a1d29ce28e959ae8dcbfaf617 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 11 Feb 2019 23:30:49 -0500 Subject: [PATCH 5/5] fix: set the existing channels unregistered --- irc/accounts.go | 12 +++++++++++- irc/channel.go | 5 ++++- irc/channelreg.go | 11 ----------- irc/chanserv.go | 14 ++++++++------ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index ccf3a426..7dbbfb09 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -842,8 +842,18 @@ func (am *AccountManager) Unregister(account string) error { var clients []*Client var registeredChannels []string + // on our way out, unregister all the account's channels and delete them from the db defer func() { - am.server.channelRegistry.deleteByAccount(casefoldedAccount, registeredChannels) + 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 diff --git a/irc/channel.go b/irc/channel.go index ab83e20b..30b9785b 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 e9792335..7b05bccc 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -215,17 +215,6 @@ func (reg *ChannelRegistry) Delete(casefoldedName string, info RegisteredChannel }) } -// deleteByAccount is a helper to delete all channel registrations corresponding to a user account. -func (reg *ChannelRegistry) deleteByAccount(cfaccount string, cfchannels []string) { - for _, cfchannel := range cfchannels { - info := reg.LoadChannel(cfchannel) - if info == nil || info.Founder != cfaccount { - continue - } - reg.Delete(cfchannel, *info) - } -} - // Rename handles the persistence part of a channel rename: the channel is // persisted under its new name, and the old name is cleaned up if necessary. func (reg *ChannelRegistry) Rename(channel *Channel, casefoldedOldName string) { diff --git a/irc/chanserv.go b/irc/chanserv.go index 333b3975..cd7e125f 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -277,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 @@ -295,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)) }