diff --git a/default.yaml b/default.yaml index 242ab7bf..e6739f93 100644 --- a/default.yaml +++ b/default.yaml @@ -1111,6 +1111,11 @@ metadata: max-subs: 100 # how many keys can be stored per entity? max-keys: 100 + # rate limiting for client metadata updates, which are expensive to process + client-throttle: + enabled: true + duration: 2m + max-attempts: 10 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. diff --git a/irc/client.go b/irc/client.go index f5f79cb4..5ce22b44 100644 --- a/irc/client.go +++ b/irc/client.go @@ -130,6 +130,7 @@ type Client struct { pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0 pushQueue pushQueue metadata map[string]string + metadataThrottle connection_limits.ThrottleDetails } type saslStatus struct { diff --git a/irc/config.go b/irc/config.go index 714c8527..634ac5f5 100644 --- a/irc/config.go +++ b/irc/config.go @@ -734,10 +734,11 @@ type Config struct { } Metadata struct { - Enabled bool - MaxSubs int `yaml:"max-subs"` - MaxKeys int `yaml:"max-keys"` - MaxValueBytes int `yaml:"max-value-length"` + Enabled bool + MaxSubs int `yaml:"max-subs"` + MaxKeys int `yaml:"max-keys"` + MaxValueBytes int `yaml:"max-value-length"` + ClientThrottle ThrottleConfig `yaml:"client-throttle"` } WebPush struct { diff --git a/irc/getters.go b/irc/getters.go index 27b08d74..e5bd8e7d 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -11,6 +11,7 @@ import ( "time" "github.com/ergochat/ergo/irc/caps" + "github.com/ergochat/ergo/irc/connection_limits" "github.com/ergochat/ergo/irc/languages" "github.com/ergochat/ergo/irc/modes" "github.com/ergochat/ergo/irc/utils" @@ -1039,3 +1040,22 @@ func (client *Client) CountMetadata() int { return len(client.metadata) } + +func (client *Client) checkMetadataThrottle() (throttled bool, remainingTime time.Duration) { + config := client.server.Config() + if !config.Metadata.ClientThrottle.Enabled { + return false, 0 + } + + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + // copy client.metadataThrottle locally and then back for processing + var throttle connection_limits.GenericThrottle + throttle.ThrottleDetails = client.metadataThrottle + throttle.Duration = config.Metadata.ClientThrottle.Duration + throttle.Limit = config.Metadata.ClientThrottle.MaxAttempts + throttled, remainingTime = throttle.Touch() + client.metadataThrottle = throttle.ThrottleDetails + return +} diff --git a/irc/handlers.go b/irc/handlers.go index 7b455028..3ffc505c 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3197,6 +3197,18 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string return } + // only rate limit clients changing their own metadata: + // channel metadata updates are not any more costly than a PRIVMSG + if client == targetClient { + if throttled, remainingTime := client.checkMetadataThrottle(); throttled { + retryAfter := strconv.Itoa(int(remainingTime.Seconds()) + 1) + rb.Add(nil, server.name, "FAIL", "METADATA", "RATE_LIMITED", + target, utils.SafeErrorParam(key), retryAfter, + fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime.Round(time.Millisecond))) + return + } + } + if len(params) > 3 { value := params[3] diff --git a/irc/metadata.go b/irc/metadata.go index b5e9a5a2..80b0b063 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -21,7 +21,7 @@ var ( errMetadataNotFound = errors.New("key not found") ) -type MetadataHaver = interface { +type MetadataHaver interface { SetMetadata(key string, value string, limit int) (updated bool, err error) GetMetadata(key string) (string, bool) DeleteMetadata(key string) (updated bool) diff --git a/irc/numerics.go b/irc/numerics.go index 339080e1..fb2c1f07 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -183,12 +183,13 @@ const ( RPL_MONLIST = "732" RPL_ENDOFMONLIST = "733" ERR_MONLISTFULL = "734" - RPL_KEYVALUE = "761" // metadata numerics + RPL_WHOISKEYVALUE = "760" // metadata numerics + RPL_KEYVALUE = "761" RPL_KEYNOTSET = "766" RPL_METADATASUBOK = "770" RPL_METADATAUNSUBOK = "771" RPL_METADATASUBS = "772" - RPL_METADATASYNCLATER = "774" + RPL_METADATASYNCLATER = "774" // end metadata numerics RPL_LOGGEDIN = "900" RPL_LOGGEDOUT = "901" ERR_NICKLOCKED = "902" diff --git a/traditional.yaml b/traditional.yaml index 5955cdf6..bdb7f497 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -1082,6 +1082,11 @@ metadata: max-subs: 100 # how many keys can be stored per entity? max-keys: 100 + # rate limiting for client metadata updates, which are expensive to process + client-throttle: + enabled: true + duration: 2m + max-attempts: 10 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications.