Enable flood control in all websocket chat rooms
This commit is contained in:
parent
3530158d2e
commit
e1abc9b7fb
|
@ -51,7 +51,8 @@ final class CoreEnv private (application: Application, val settings: Settings) {
|
|||
userRepo = user.userRepo,
|
||||
getGame = game.gameRepo.game,
|
||||
roundSocket = round.socket,
|
||||
roundMessenger = round.messenger)
|
||||
roundMessenger = round.messenger,
|
||||
flood = security.flood)
|
||||
|
||||
lazy val setup = new lila.setup.SetupEnv(
|
||||
settings = settings,
|
||||
|
@ -85,7 +86,8 @@ final class CoreEnv private (application: Application, val settings: Settings) {
|
|||
userRepo = user.userRepo,
|
||||
eloUpdater = user.eloUpdater,
|
||||
i18nKeys = i18n.keys,
|
||||
ai = ai.ai)
|
||||
ai = ai.ai,
|
||||
flood = security.flood)
|
||||
|
||||
lazy val analyse = new lila.analyse.AnalyseEnv(
|
||||
settings = settings,
|
||||
|
|
|
@ -13,6 +13,7 @@ import timeline.Entry
|
|||
import user.{ User, UserRepo }
|
||||
import game.DbGame
|
||||
import round.{ Socket ⇒ RoundSocket, Messenger ⇒ RoundMessenger }
|
||||
import security.Flood
|
||||
import core.Settings
|
||||
|
||||
final class LobbyEnv(
|
||||
|
@ -22,7 +23,8 @@ final class LobbyEnv(
|
|||
userRepo: UserRepo,
|
||||
getGame: String => IO[Option[DbGame]],
|
||||
roundSocket: RoundSocket,
|
||||
roundMessenger: RoundMessenger) {
|
||||
roundMessenger: RoundMessenger,
|
||||
flood: Flood) {
|
||||
|
||||
implicit val ctx = app
|
||||
import settings._
|
||||
|
@ -39,7 +41,9 @@ final class LobbyEnv(
|
|||
timeout = SiteUidTimeout
|
||||
)), name = ActorLobbyHub)
|
||||
|
||||
lazy val socket = new Socket(hub = hub)
|
||||
lazy val socket = new Socket(
|
||||
hub = hub,
|
||||
flood = flood)
|
||||
|
||||
lazy val fisherman = new Fisherman(
|
||||
hookRepo = hookRepo,
|
||||
|
|
|
@ -14,8 +14,9 @@ import implicits.RichJs._
|
|||
import socket.{ Util, PingVersion, Quit }
|
||||
import timeline.Entry
|
||||
import game.DbGame
|
||||
import security.Flood
|
||||
|
||||
final class Socket(hub: ActorRef) {
|
||||
final class Socket(hub: ActorRef, flood: Flood) {
|
||||
|
||||
implicit val timeout = Timeout(1 second)
|
||||
|
||||
|
@ -34,6 +35,7 @@ final class Socket(hub: ActorRef) {
|
|||
case Some("talk") ⇒ for {
|
||||
data ← e obj "d"
|
||||
txt ← data str "txt"
|
||||
if flood.allowMessage(uid, txt)
|
||||
uname ← username
|
||||
} hub ! Talk(txt, uname)
|
||||
case Some("p") ⇒ e int "v" foreach { v ⇒
|
||||
|
|
|
@ -20,17 +20,14 @@ object Builder {
|
|||
.removalListener(onRemove)
|
||||
.build[K, V](f)
|
||||
|
||||
def expiry[K, V](ttl: Int): Cache[K, V] =
|
||||
cacheBuilder[K, V](ttl).build[K, V]
|
||||
|
||||
private def cacheBuilder[K, V](ttl: Int): CacheBuilder[K, V] =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(ttl, TimeUnit.MILLISECONDS)
|
||||
.asInstanceOf[CacheBuilder[K, V]]
|
||||
|
||||
def expiry[K, V](ttl: Int): Cache[K, V] =
|
||||
CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(ttl, TimeUnit.MILLISECONDS)
|
||||
.asInstanceOf[CacheBuilder[K, V]]
|
||||
.build[K, V]
|
||||
|
||||
implicit def functionToRemovalListener[K, V](f: (K, V) ⇒ Unit): RemovalListener[K, V] =
|
||||
new RemovalListener[K, V] {
|
||||
def onRemoval(notification: RemovalNotification[K, V]) =
|
||||
|
|
|
@ -65,7 +65,9 @@ final class Hub(
|
|||
sender ! Connected(enumerator, member)
|
||||
}
|
||||
|
||||
case Events(Nil) ⇒
|
||||
case Events(events) ⇒ notify(events)
|
||||
case GameEvents(_, Nil) ⇒
|
||||
case GameEvents(_, events) ⇒ notify(events)
|
||||
|
||||
case Quit(uid) ⇒ {
|
||||
|
@ -86,7 +88,7 @@ final class Hub(
|
|||
.filter(_.watcher)
|
||||
.map(_.username)
|
||||
.toList.partition(_.isDefined) match {
|
||||
case (users, anons) ⇒ users.flatten.distinct |> { userList =>
|
||||
case (users, anons) ⇒ users.flatten.distinct |> { userList ⇒
|
||||
anons.size match {
|
||||
case 0 ⇒ userList
|
||||
case 1 ⇒ userList :+ "Anonymous"
|
||||
|
|
|
@ -13,6 +13,7 @@ import elo.EloUpdater
|
|||
import ai.Ai
|
||||
import core.Settings
|
||||
import i18n.I18nKeys
|
||||
import security.Flood
|
||||
|
||||
final class RoundEnv(
|
||||
app: Application,
|
||||
|
@ -22,7 +23,8 @@ final class RoundEnv(
|
|||
userRepo: UserRepo,
|
||||
eloUpdater: EloUpdater,
|
||||
i18nKeys: I18nKeys,
|
||||
ai: () ⇒ Ai) {
|
||||
ai: () ⇒ Ai,
|
||||
flood: Flood) {
|
||||
|
||||
implicit val ctx = app
|
||||
import settings._
|
||||
|
@ -41,7 +43,8 @@ final class RoundEnv(
|
|||
getPlayerPov = gameRepo.pov,
|
||||
hand = hand,
|
||||
hubMaster = hubMaster,
|
||||
messenger = messenger)
|
||||
messenger = messenger,
|
||||
flood = flood)
|
||||
|
||||
lazy val hand = new Hand(
|
||||
gameRepo = gameRepo,
|
||||
|
|
|
@ -18,6 +18,7 @@ import game.{ Pov, PovRef }
|
|||
import chess.Color
|
||||
import socket.{ PingVersion, Quit }
|
||||
import socket.Util.connectionFail
|
||||
import security.Flood
|
||||
import implicits.RichJs._
|
||||
|
||||
final class Socket(
|
||||
|
@ -25,7 +26,8 @@ final class Socket(
|
|||
getPlayerPov: String ⇒ IO[Option[Pov]],
|
||||
hand: Hand,
|
||||
hubMaster: ActorRef,
|
||||
messenger: Messenger) {
|
||||
messenger: Messenger,
|
||||
flood: Flood) {
|
||||
|
||||
private val timeoutDuration = 1 second
|
||||
implicit private val timeout = Timeout(timeoutDuration)
|
||||
|
@ -50,7 +52,10 @@ final class Socket(
|
|||
case Some("p") ⇒ e int "v" foreach { v ⇒
|
||||
hub ! PingVersion(uid, v)
|
||||
}
|
||||
case Some("talk") ⇒ e str "d" foreach { txt ⇒
|
||||
case Some("talk") ⇒ for {
|
||||
txt ← e str "d"
|
||||
if flood.allowMessage(uid, txt)
|
||||
} {
|
||||
val events = messenger.playerMessage(povRef, txt).unsafePerformIO
|
||||
hub ! Events(events)
|
||||
}
|
||||
|
@ -75,9 +80,12 @@ final class Socket(
|
|||
case Some("p") ⇒ e int "v" foreach { v ⇒
|
||||
hub ! PingVersion(uid, v)
|
||||
}
|
||||
case Some("talk") ⇒ e str "d" foreach { txt ⇒
|
||||
case Some("talk") ⇒ for {
|
||||
txt ← e str "d"
|
||||
if flood.allowMessage(uid, txt)
|
||||
} {
|
||||
val events = messenger.watcherMessage(
|
||||
povRef.gameId,
|
||||
povRef.gameId,
|
||||
member.username,
|
||||
txt).unsafePerformIO
|
||||
hub ! Events(events)
|
||||
|
|
50
app/security/Flood.scala
Normal file
50
app/security/Flood.scala
Normal file
|
@ -0,0 +1,50 @@
|
|||
package lila
|
||||
package security
|
||||
|
||||
import memo.Builder
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import org.scala_tools.time.Imports._
|
||||
|
||||
final class Flood {
|
||||
|
||||
private val ttl = 60 * 1000
|
||||
private val floodNumber = 4
|
||||
private val floodDelay = 10 * 1000
|
||||
|
||||
case class Message(text: String, date: DateTime) {
|
||||
|
||||
def same(other: Message) = this.text == other.text
|
||||
}
|
||||
type Messages = List[Message]
|
||||
|
||||
// uid, [date, message]
|
||||
private val messages = Builder.expiry[String, Messages](ttl)
|
||||
|
||||
def filterMessage[A](uid: String, text: String)(op: ⇒ Unit) {
|
||||
if (allowMessage(uid, text)) op
|
||||
}
|
||||
|
||||
def allowMessage(uid: String, text: String): Boolean = {
|
||||
val msg = Message(text, DateTime.now)
|
||||
val msgs = ~Option(messages getIfPresent uid)
|
||||
val allow = !duplicateMessage(msg, msgs) && !quickPost(msg, msgs)
|
||||
allow ~ { a ⇒
|
||||
if (a) messages.put(uid, msg :: msgs)
|
||||
}
|
||||
}
|
||||
|
||||
private def duplicateMessage(msg: Message, msgs: Messages): Boolean =
|
||||
msgs.headOption.fold(
|
||||
m ⇒ (m same msg) || msgs.tail.headOption.fold(
|
||||
_ same msg,
|
||||
false
|
||||
),
|
||||
false)
|
||||
|
||||
private def quickPost(msg: Message, msgs: Messages): Boolean =
|
||||
(msgs lift floodNumber).fold(
|
||||
old ⇒ old.date > (msg.date - floodDelay.millis),
|
||||
false
|
||||
)
|
||||
}
|
|
@ -21,6 +21,8 @@ final class SecurityEnv(
|
|||
lazy val firewall = new security.Firewall(
|
||||
collection = mongodb(MongoCollectionFirewall),
|
||||
cacheTtl = FirewallCacheTtl)
|
||||
|
||||
lazy val flood = new security.Flood
|
||||
|
||||
lazy val forms = new DataForm(
|
||||
userRepo = userRepo,
|
||||
|
|
Loading…
Reference in a new issue