diff --git a/app/controllers/Setup.scala b/app/controllers/Setup.scala index 64feb7de27..c1fa47172b 100644 --- a/app/controllers/Setup.scala +++ b/app/controllers/Setup.scala @@ -5,6 +5,7 @@ import play.api.i18n.Messages.Implicits._ import play.api.libs.json.Json import play.api.mvc.{ Result, Results, Call, RequestHeader, Accepting } import play.api.Play.current +import scala.concurrent.duration._ import lila.api.{ Context, BodyContext } import lila.app._ @@ -18,6 +19,9 @@ object Setup extends LilaController with TheftPrevention { private def env = Env.setup + private val FormRateLimit = new lila.security.RateLimitByKey(1 second) + private val PostRateLimit = new lila.security.RateLimitByKey(1 second) + def aiForm = Open { implicit ctx => if (HTTPRequest isXhr ctx.req) { env.forms aiFilled get("fen") map { form => @@ -88,8 +92,10 @@ object Setup extends LilaController with TheftPrevention { } def hookForm = Open { implicit ctx => - if (HTTPRequest isXhr ctx.req) NoPlaybanOrCurrent { - env.forms.hookFilled(timeModeString = get("time")) map { html.setup.hook(_) } + if (HTTPRequest isXhr ctx.req) FormRateLimit(ctx.req.remoteAddress) { + NoPlaybanOrCurrent { + env.forms.hookFilled(timeModeString = get("time")) map { html.setup.hook(_) } + } } else fuccess { Redirect(routes.Lobby.home + "#hook") @@ -114,31 +120,35 @@ object Setup extends LilaController with TheftPrevention { def hook(uid: String) = OpenBody { implicit ctx => implicit val req = ctx.body - NoPlaybanOrCurrent { - env.forms.hook(ctx).bindFromRequest.fold( - err => negotiate( - html = BadRequest(err.errorsAsJson.toString).fuccess, - api = _ => BadRequest(err.errorsAsJson).fuccess), - preConfig => (ctx.userId ?? Env.relation.api.blocking) zip - mobileHookAllowAnon(preConfig) flatMap { - case (blocking, config) => - env.processor.hook(config, uid, HTTPRequest sid req, blocking) map hookResponse recover { - case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON - } - } - ) + PostRateLimit(req.remoteAddress) { + NoPlaybanOrCurrent { + env.forms.hook(ctx).bindFromRequest.fold( + err => negotiate( + html = BadRequest(err.errorsAsJson.toString).fuccess, + api = _ => BadRequest(err.errorsAsJson).fuccess), + preConfig => (ctx.userId ?? Env.relation.api.blocking) zip + mobileHookAllowAnon(preConfig) flatMap { + case (blocking, config) => + env.processor.hook(config, uid, HTTPRequest sid req, blocking) map hookResponse recover { + case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON + } + } + ) + } } } def like(uid: String, gameId: String) = Open { implicit ctx => - NoPlaybanOrCurrent { - env.forms.hookConfig flatMap { config => - GameRepo game gameId map { - _.fold(config)(config.updateFrom) - } flatMap { config => - (ctx.userId ?? Env.relation.api.blocking) flatMap { blocking => - env.processor.hook(config, uid, HTTPRequest sid ctx.req, blocking) map hookResponse recover { - case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON + PostRateLimit(ctx.req.remoteAddress) { + NoPlaybanOrCurrent { + env.forms.hookConfig flatMap { config => + GameRepo game gameId map { + _.fold(config)(config.updateFrom) + } flatMap { config => + (ctx.userId ?? Env.relation.api.blocking) flatMap { blocking => + env.processor.hook(config, uid, HTTPRequest sid ctx.req, blocking) map hookResponse recover { + case e: IllegalArgumentException => BadRequest(Json.obj("error" -> e.getMessage)) as JSON + } } } } diff --git a/modules/security/src/main/RateLimit.scala b/modules/security/src/main/RateLimit.scala index 6db41c4cef..e3fda92dee 100644 --- a/modules/security/src/main/RateLimit.scala +++ b/modules/security/src/main/RateLimit.scala @@ -1,21 +1,41 @@ package lila.security -import scala.concurrent.duration.Duration import lila.memo.ExpireSetMemo +import scala.concurrent.duration.Duration +import ornicar.scalalib.Zero -/** very simple side effect throttler - that allows one call per duration, - and discards the rest */ +/** + * very simple side effect throttler + * that allows one call per duration, + * and discards the rest + */ final class RateLimitGlobal(duration: Duration) { private val durationMillis = duration.toMillis private var lastHit: Long = nowMillis - durationMillis - 10 - def apply(op: => Unit) { - if (nowMillis > lastHit + durationMillis) { + def apply[A: Zero](op: => A): A = { + (nowMillis > lastHit + durationMillis) ?? { lastHit = nowMillis op } } } + +/** + * very simple side effect throttler + * that allows one call per duration and per key, + * and discards the rest + */ +final class RateLimitByKey(duration: Duration) { + + private val storage = new ExpireSetMemo(ttl = duration) + + def apply[A: Zero](key: String)(op: => A): A = { + (!storage.get(key)) ?? { + storage put key + op + } + } +}