exempt a configurable number of MARKREAD commands from fakelag

This commit is contained in:
Shivaram Lingamneni 2022-08-22 23:23:17 -04:00
parent 7d6ff58bf8
commit 7ad31497c2
6 changed files with 59 additions and 12 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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) {

View File

@ -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)

View File

@ -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")
}

View File

@ -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.