rate limit hook form and post requests to prevent playban flood

This commit is contained in:
Thibault Duplessis 2015-11-05 09:09:18 +07:00
parent 9345227579
commit 8885265ba6
2 changed files with 59 additions and 29 deletions

View file

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

View file

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