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:
parent
ca72dc28c0
commit
67b3143893
@ -5,6 +5,7 @@ import (
|
|||||||
"html"
|
"html"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/zorchenhimer/MovieNight/common"
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
@ -25,6 +26,36 @@ type Client struct {
|
|||||||
IsColorForced bool
|
IsColorForced bool
|
||||||
IsNameForced bool
|
IsNameForced bool
|
||||||
regexName *regexp.Regexp
|
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
|
//Client has a new message to broadcast
|
||||||
@ -85,11 +116,42 @@ func (cl *Client) NewMsg(data common.ClientData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} 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
|
// Trim long messages
|
||||||
if len(msg) > 400 {
|
if len(msg) > 400 {
|
||||||
msg = msg[0: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)
|
common.LogChatf("[chat] <%s> %q\n", cl.name, msg)
|
||||||
|
|
||||||
// Enable links for mods and admins
|
// Enable links for mods and admins
|
||||||
@ -188,6 +250,7 @@ func (cl *Client) setName(s string) error {
|
|||||||
|
|
||||||
cl.name = s
|
cl.name = s
|
||||||
cl.regexName = regex
|
cl.regexName = regex
|
||||||
|
cl.conn.clientName = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/zorchenhimer/MovieNight/common"
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
)
|
)
|
||||||
@ -126,6 +127,10 @@ var commands = &CommandControl{
|
|||||||
return "You are not allowed to change your color."
|
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 {
|
if len(args) == 0 {
|
||||||
cl.setColor(common.RandomColor())
|
cl.setColor(common.RandomColor())
|
||||||
return "Random color chosen: " + cl.color
|
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."
|
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])
|
err := cl.setColor(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogErrorf("[color] could not send color update to client: %v\n", err)
|
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."
|
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, " "))
|
pw := html.UnescapeString(strings.Join(args, " "))
|
||||||
|
|
||||||
if settings.AdminPassword == pw {
|
if settings.AdminPassword == pw {
|
||||||
@ -196,6 +211,12 @@ var commands = &CommandControl{
|
|||||||
common.CNNick.String(): Command{
|
common.CNNick.String(): Command{
|
||||||
HelpText: "Change display name",
|
HelpText: "Change display name",
|
||||||
Function: func(cl *Client, args []string) string {
|
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 {
|
if len(args) == 0 {
|
||||||
return "Missing name to change to."
|
return "Missing name to change to."
|
||||||
}
|
}
|
||||||
|
11
chatroom.go
11
chatroom.go
@ -96,16 +96,9 @@ func (cr *ChatRoom) Join(name, uid string) (*Client, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.clientName = name
|
client, err := NewClient(conn, cr, name, common.RandomColor())
|
||||||
client := &Client{
|
|
||||||
conn: conn,
|
|
||||||
belongsTo: cr,
|
|
||||||
color: common.RandomColor(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.setName(name)
|
|
||||||
if err != nil {
|
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()
|
host := client.Host()
|
||||||
|
43
settings.go
43
settings.go
@ -33,6 +33,13 @@ type Settings struct {
|
|||||||
LogLevel common.LogLevel
|
LogLevel common.LogLevel
|
||||||
LogFile string
|
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?
|
// Send the NoCache header?
|
||||||
NoCache bool
|
NoCache bool
|
||||||
}
|
}
|
||||||
@ -72,6 +79,42 @@ func LoadSettings(filename string) (*Settings, error) {
|
|||||||
return nil, fmt.Errorf("unable to generate admin password: %s", err)
|
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().
|
// 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)
|
fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user