Add rate limiting

This should fix #65, although it may be expanded in the future.

Default settings for limits can be changed or disabled in the settings.
Default values:
- Chat: 1 second
- Duplicate chat: 30 seconds
- /nick: 5 minutes
- /auth: 5 seconds
- /color: 1 minute

Creation of the chat Client object has been moved from ChatRoom.Join()
to NewClient().  This function also handles setting the initial name.
This commit is contained in:
Zorchenhimer 2019-03-30 15:39:04 -04:00
parent ca72dc28c0
commit 67b3143893
4 changed files with 129 additions and 9 deletions

View File

@ -5,6 +5,7 @@ import (
"html"
"regexp"
"strings"
"time"
"unicode"
"github.com/zorchenhimer/MovieNight/common"
@ -25,6 +26,36 @@ type Client struct {
IsColorForced bool
IsNameForced bool
regexName *regexp.Regexp
// Times since last event. use time.Duration.Since()
nextChat time.Time // rate limit chat messages
nextNick time.Time // rate limit nickname changes
nextColor time.Time // rate limit color changes
nextAuth time.Time // rate limit failed auth attempts. Sould prolly have a backoff policy.
authTries int // number of failed auth attempts
nextDuplicate time.Time
lastMsg string
}
func NewClient(connection *chatConnection, room *ChatRoom, name, color string) (*Client, error) {
c := &Client{
conn: connection,
belongsTo: room,
color: color,
}
if err := c.setName(name); err != nil {
return nil, fmt.Errorf("could not set client name to %#v: %v", name, err)
}
// Set initial vaules to their rate limit duration in the past.
c.nextChat = time.Now()
c.nextNick = time.Now()
c.nextColor = time.Now()
c.nextAuth = time.Now()
return c, nil
}
//Client has a new message to broadcast
@ -85,11 +116,42 @@ func (cl *Client) NewMsg(data common.ClientData) {
}
} else {
// Limit the rate of sent chat messages. Ignore mods and admins
if time.Now().Before(cl.nextChat) && cl.CmdLevel == common.CmdlUser {
err := cl.SendChatData(common.NewChatMessage("", "",
"Slow down.",
common.CmdlUser,
common.MsgCommandResponse))
if err != nil {
common.LogErrorf("Unable to send slowdown for chat: %v", err)
}
return
}
// Trim long messages
if len(msg) > 400 {
msg = msg[0:400]
}
// Limit the rate of duplicate messages. Ignore mods and admins.
// Only checks the last message.
if strings.TrimSpace(strings.ToLower(msg)) == cl.lastMsg &&
time.Now().Before(cl.nextDuplicate) &&
cl.CmdLevel == common.CmdlUser {
err := cl.SendChatData(common.NewChatMessage("", "",
common.ParseEmotes("You already sent that PeepoSus"),
common.CmdlUser,
common.MsgCommandResponse))
if err != nil {
common.LogErrorf("Unable to send slowdown for chat: %v", err)
}
return
}
cl.nextChat = time.Now().Add(time.Second * settings.RateLimitChat)
cl.nextDuplicate = time.Now().Add(time.Second * settings.RateLimitDuplicate)
cl.lastMsg = strings.TrimSpace(strings.ToLower(msg))
common.LogChatf("[chat] <%s> %q\n", cl.name, msg)
// Enable links for mods and admins
@ -188,6 +250,7 @@ func (cl *Client) setName(s string) error {
cl.name = s
cl.regexName = regex
cl.conn.clientName = s
return nil
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"html"
"strings"
"time"
"github.com/zorchenhimer/MovieNight/common"
)
@ -126,6 +127,10 @@ var commands = &CommandControl{
return "You are not allowed to change your color."
}
if time.Now().Before(cl.nextColor) && cl.CmdLevel == common.CmdlUser {
return fmt.Sprintf("Slow down. You can change your color in %0.0f seconds.", time.Until(cl.nextColor).Seconds())
}
if len(args) == 0 {
cl.setColor(common.RandomColor())
return "Random color chosen: " + cl.color
@ -136,6 +141,8 @@ var commands = &CommandControl{
return "To choose a specific color use the format <i>/color #c029ce</i>. Hex values expected."
}
cl.nextColor = time.Now().Add(time.Second * settings.RateLimitColor)
err := cl.setColor(args[0])
if err != nil {
common.LogErrorf("[color] could not send color update to client: %v\n", err)
@ -163,6 +170,14 @@ var commands = &CommandControl{
return "You are already authenticated."
}
// TODO: handle backoff policy
if time.Now().Before(cl.nextAuth) {
cl.nextAuth = time.Now().Add(time.Second * settings.RateLimitAuth)
return "Slow down."
}
cl.authTries += 1 // this isn't used yet
cl.nextAuth = time.Now().Add(time.Second * settings.RateLimitAuth)
pw := html.UnescapeString(strings.Join(args, " "))
if settings.AdminPassword == pw {
@ -196,6 +211,12 @@ var commands = &CommandControl{
common.CNNick.String(): Command{
HelpText: "Change display name",
Function: func(cl *Client, args []string) string {
if time.Now().Before(cl.nextNick) {
//cl.nextNick = time.Now().Add(time.Second * settings.RateLimitNick)
return fmt.Sprintf("Slow down. You can change your nick in %0.0f seconds.", time.Until(cl.nextNick).Seconds())
}
cl.nextNick = time.Now().Add(time.Second * settings.RateLimitNick)
if len(args) == 0 {
return "Missing name to change to."
}

View File

@ -96,16 +96,9 @@ func (cr *ChatRoom) Join(name, uid string) (*Client, error) {
}
}
conn.clientName = name
client := &Client{
conn: conn,
belongsTo: cr,
color: common.RandomColor(),
}
err := client.setName(name)
client, err := NewClient(conn, cr, name, common.RandomColor())
if err != nil {
return nil, fmt.Errorf("could not set client name to %#v: %v", name, err)
return nil, fmt.Errorf("Unable to join client: %v", err)
}
host := client.Host()

View File

@ -33,6 +33,13 @@ type Settings struct {
LogLevel common.LogLevel
LogFile string
// Rate limiting stuff, in seconds
RateLimitChat time.Duration
RateLimitNick time.Duration
RateLimitColor time.Duration
RateLimitAuth time.Duration
RateLimitDuplicate time.Duration // Amount of seconds between allowed duplicate messages
// Send the NoCache header?
NoCache bool
}
@ -72,6 +79,42 @@ func LoadSettings(filename string) (*Settings, error) {
return nil, fmt.Errorf("unable to generate admin password: %s", err)
}
if s.RateLimitChat == -1 {
s.RateLimitChat = 0
} else if s.RateLimitChat <= 0 {
s.RateLimitChat = 1
}
if s.RateLimitNick == -1 {
s.RateLimitNick = 0
} else if s.RateLimitNick <= 0 {
s.RateLimitNick = 300
}
if s.RateLimitColor == -1 {
s.RateLimitColor = 0
} else if s.RateLimitColor <= 0 {
s.RateLimitColor = 60
}
if s.RateLimitAuth == -1 {
s.RateLimitAuth = 0
} else if s.RateLimitAuth <= 0 {
s.RateLimitAuth = 5
}
if s.RateLimitDuplicate == -1 {
s.RateLimitDuplicate = 0
} else if s.RateLimitDuplicate <= 0 {
s.RateLimitDuplicate = 30
}
// Print this stuff before we multiply it by time.Second
common.LogInfof("RateLimitChat: %v", s.RateLimitChat)
common.LogInfof("RateLimitNick: %v", s.RateLimitNick)
common.LogInfof("RateLimitColor: %v", s.RateLimitColor)
common.LogInfof("RateLimitAuth: %v", s.RateLimitAuth)
// Don't use LogInfof() here. Log isn't setup yet when LoadSettings() is called from init().
fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword)