From 7ad31497c2fb9fed2f990fe0749c8a594ad1b391 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 22 Aug 2022 23:23:17 -0400 Subject: [PATCH] exempt a configurable number of MARKREAD commands from fakelag --- default.yaml | 8 ++++++++ irc/client.go | 18 +++++++++++++----- irc/config.go | 12 ++++++++++++ irc/fakelag.go | 13 ++++++++++++- irc/fakelag_test.go | 12 ++++++------ traditional.yaml | 8 ++++++++ 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/default.yaml b/default.yaml index 8da88e09..50425552 100644 --- a/default.yaml +++ b/default.yaml @@ -849,6 +849,14 @@ fakelag: # sending any commands: cooldown: 2s + # exempt a certain number of command invocations per session from fakelag; + # this is to speed up "resynchronization" of client state during reattach + command-budgets: + "CHATHISTORY": 16 + "MARKREAD": 16 + "MONITOR": 1 + "WHO": 4 + # the roleplay commands are semi-standardized extensions to IRC that allow # sending and receiving messages from pseudo-nicknames. this can be used either # for actual roleplaying, or for bridging IRC with other protocols. diff --git a/irc/client.go b/irc/client.go index a9cf5b05..55d3d1fd 100644 --- a/irc/client.go +++ b/irc/client.go @@ -668,12 +668,21 @@ func (client *Client) run(session *Session) { } } + msg, err := ircmsg.ParseLineStrict(line, true, MaxLineLen) + // XXX defer processing of command error parsing until after fakelag + if client.registered { - touches := session.deferredFakelagCount + 1 - session.deferredFakelagCount = 0 - for i := 0; i < touches; i++ { - session.fakelag.Touch() + // apply deferred fakelag + for i := 0; i < session.deferredFakelagCount; i++ { + session.fakelag.Touch("") } + session.deferredFakelagCount = 0 + // touch for the current command + var command string + if err == nil { + command = msg.Command + } + session.fakelag.Touch(command) } else { // DoS hardening, #505 session.registrationMessages++ @@ -683,7 +692,6 @@ func (client *Client) run(session *Session) { } } - msg, err := ircmsg.ParseLineStrict(line, true, MaxLineLen) if err == ircmsg.ErrorLineIsEmpty { continue } else if err == ircmsg.ErrorTagsTooLong { diff --git a/irc/config.go b/irc/config.go index 75fd1f27..9e9cb877 100644 --- a/irc/config.go +++ b/irc/config.go @@ -524,6 +524,7 @@ type FakelagConfig struct { BurstLimit uint `yaml:"burst-limit"` MessagesPerWindow uint `yaml:"messages-per-window"` Cooldown time.Duration + CommandBudgets map[string]int `yaml:"command-budgets"` } type TorListenersConfig struct { @@ -1428,6 +1429,17 @@ func LoadConfig(filename string) (config *Config, err error) { } config.Server.capValues[caps.Languages] = config.languageManager.CapValue() + if len(config.Fakelag.CommandBudgets) != 0 { + // normalize command names to uppercase: + commandBudgets := make(map[string]int, len(config.Fakelag.CommandBudgets)) + for command, budget := range config.Fakelag.CommandBudgets { + commandBudgets[strings.ToUpper(command)] = budget + } + config.Fakelag.CommandBudgets = commandBudgets + } else { + config.Fakelag.CommandBudgets = nil + } + if config.Server.Relaymsg.Enabled { for _, char := range protocolBreakingNameCharacters { if strings.ContainsRune(config.Server.Relaymsg.Separators, char) { diff --git a/irc/fakelag.go b/irc/fakelag.go index 8f3a4c9e..25ef0609 100644 --- a/irc/fakelag.go +++ b/irc/fakelag.go @@ -5,6 +5,8 @@ package irc import ( "time" + + "github.com/ergochat/ergo/irc/utils" ) // fakelag is a system for artificially delaying commands when a user issues @@ -36,6 +38,10 @@ type Fakelag struct { func (fl *Fakelag) Initialize(config FakelagConfig) { fl.config = config + // XXX don't share mutable member CommandBudgets: + if config.CommandBudgets != nil { + fl.config.CommandBudgets = utils.CopyMap(config.CommandBudgets) + } fl.nowFunc = time.Now fl.sleepFunc = time.Sleep fl.state = FakelagBursting @@ -58,11 +64,16 @@ func (fl *Fakelag) Unsuspend() { } // register a new command, sleep if necessary to delay it -func (fl *Fakelag) Touch() { +func (fl *Fakelag) Touch(command string) { if !fl.config.Enabled { return } + if budget, ok := fl.config.CommandBudgets[command]; ok && budget > 0 { + fl.config.CommandBudgets[command] = budget - 1 + return + } + now := fl.nowFunc() // XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine elapsed := now.Sub(fl.lastTouch) diff --git a/irc/fakelag_test.go b/irc/fakelag_test.go index bc1ff45b..e1079466 100644 --- a/irc/fakelag_test.go +++ b/irc/fakelag_test.go @@ -60,7 +60,7 @@ func TestFakelag(t *testing.T) { window, _ := time.ParseDuration("1s") fl, mt := newFakelagForTesting(window, 3, 2, window) - fl.Touch() + fl.Touch("") slept, _ := mt.lastSleep() if slept { t.Fatalf("should not have slept") @@ -69,7 +69,7 @@ func TestFakelag(t *testing.T) { interval, _ := time.ParseDuration("100ms") for i := 0; i < 2; i++ { mt.pause(interval) - fl.Touch() + fl.Touch("") slept, _ := mt.lastSleep() if slept { t.Fatalf("should not have slept") @@ -77,7 +77,7 @@ func TestFakelag(t *testing.T) { } mt.pause(interval) - fl.Touch() + fl.Touch("") if fl.state != FakelagThrottled { t.Fatalf("should be throttled") } @@ -91,7 +91,7 @@ func TestFakelag(t *testing.T) { } // send another message without a pause; we should have to sleep for 500 msec - fl.Touch() + fl.Touch("") if fl.state != FakelagThrottled { t.Fatalf("should be throttled") } @@ -102,7 +102,7 @@ func TestFakelag(t *testing.T) { } mt.pause(interval * 6) - fl.Touch() + fl.Touch("") if fl.state != FakelagThrottled { t.Fatalf("should still be throttled") } @@ -112,7 +112,7 @@ func TestFakelag(t *testing.T) { } mt.pause(window * 2) - fl.Touch() + fl.Touch("") if fl.state != FakelagBursting { t.Fatalf("should be bursting again") } diff --git a/traditional.yaml b/traditional.yaml index 7ea27137..3ea879ae 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -821,6 +821,14 @@ fakelag: # sending any commands: cooldown: 2s + # exempt a certain number of command invocations per session from fakelag; + # this is to speed up "resynchronization" of client state during reattach + command-budgets: + "CHATHISTORY": 16 + "MARKREAD": 16 + "MONITOR": 1 + "WHO": 4 + # the roleplay commands are semi-standardized extensions to IRC that allow # sending and receiving messages from pseudo-nicknames. this can be used either # for actual roleplaying, or for bridging IRC with other protocols.