remove implicit rate limiter default to ensure 429 results

pull/6642/head
Thibault Duplessis 2020-05-14 14:23:11 -06:00
parent 51a8373ad3
commit 1a137617bb
32 changed files with 98 additions and 107 deletions

View File

@ -143,7 +143,7 @@ final class Account(
Redirect(routes.Account.passwd).flashSuccess
}
}
}
}(rateLimitedFu)
}
private def emailForm(user: UserModel) =
@ -188,10 +188,10 @@ final class Account(
Redirect(routes.Account.email).flashSuccess {
lila.i18n.I18nKeys.checkYourEmail.txt()
}
}
}(rateLimitedFu)
}
}
}
}(rateLimitedFu)
}
def emailConfirm(token: String) =
@ -259,7 +259,7 @@ final class Account(
Redirect(routes.Account.twoFactor).flashSuccess
}
}
}
}(rateLimitedFu)
}
def disableTwoFactor =
@ -274,7 +274,7 @@ final class Account(
Redirect(routes.Account.twoFactor).flashSuccess
}
}
}
}(rateLimitedFu)
}
def close =
@ -395,7 +395,7 @@ final class Account(
env.security.reopen.send(user, data.realEmail) inject Redirect(
routes.Account.reopenSent(data.realEmail.value)
)
}
}(rateLimitedFu)
}
)
}

View File

@ -23,8 +23,6 @@ final class Api(
private val userApi = env.api.userApi
private val gameApi = env.api.gameApi
implicit private[controllers] val limitedDefault = Zero.instance[ApiResult](Limited)
private lazy val apiStatusJson = {
val api = lila.api.Mobile.Api
Json.obj(
@ -76,7 +74,7 @@ final class Api(
env.user.repo nameds usernames map {
_.map { env.user.jsonView(_, none) }
} map toApiResult map toHttp
}
}(rateLimitedFu)
}
def usersStatus =
@ -123,9 +121,9 @@ final class Api(
UserGamesRateLimitPerUA(~HTTPRequest.userAgent(req), cost = cost, msg = ip.value) {
UserGamesRateLimitGlobal("-", cost = cost, msg = ip.value) {
run
}
}
}
}(fuccess(Limited))
}(fuccess(Limited))
}(fuccess(Limited))
}
private def gameFlagsFromRequest(req: RequestHeader) =
@ -174,7 +172,7 @@ final class Api(
GameRateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = 1) {
lila.mon.api.game.increment(1)
gameApi.one(id take lila.game.Game.gameIdSize, gameFlagsFromRequest(req)) map toApiResult
}
}(fuccess(Limited))
}
private val CrosstableRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
@ -192,7 +190,7 @@ final class Api(
lila.game.JsonView.crosstableWrites.writes(ct).some
}
}
}
}(fuccess(Limited))
}
def currentTournaments =
@ -348,7 +346,7 @@ final class Api(
}
}
} map toApiResult
}
}(fuccess(Limited))
}
def CookieBasedApiRequest(js: Context => Fu[ApiResult]) =

View File

@ -138,7 +138,7 @@ final class Auth(
}
)
}
}
}(rateLimitedFu)
)
}
}
@ -259,7 +259,7 @@ final class Auth(
lila.security.EmailConfirm.cookie
.make(env.lilaCookie, user, newUserEmail.email)(ctx.req)
}
}
}(rateLimitedFu)
}
}
}
@ -394,7 +394,7 @@ final class Auth(
env.push.webSubscriptionApi.unsubscribeByUser(user) >>
authenticateUser(user) >>-
lila.mon.user.auth.passwordResetConfirm("success").increment()
}
}(rateLimitedFu)
}
}
}
@ -422,7 +422,7 @@ final class Auth(
env.security.magicLink.send(user, storedEmail) inject Redirect(
routes.Auth.magicLinkSent(storedEmail.value)
)
}
}(rateLimitedFu)
}
case _ => {
lila.mon.user.auth.magicLinkRequest("no_email").increment()

View File

@ -198,7 +198,7 @@ final class Challenge(
case None => api.setDestUser(c, dest) inject Redirect(routes.Challenge.show(c.id))
}
}
}
}(rateLimitedFu)
)
else notFound
}
@ -249,8 +249,8 @@ final class Challenge(
} map (_ as JSON)
}
}
}
}
}(rateLimitedFu)
}(rateLimitedFu)
)
}
@ -312,7 +312,7 @@ final class Challenge(
case false =>
BadRequest(jsonError("Challenge not created"))
}
} dmap (_ as JSON)
}(rateLimitedFu) dmap (_ as JSON)
)
}

View File

@ -451,7 +451,7 @@ final class Clas(
Redirect(routes.Clas.studentShow(clas.id.value, s.user.username)).flashSuccess {
s"A confirmation email was sent to ${email.acceptable.value}. ${s.student.realName} must click the link in the email to release the account."
}
}
}(rateLimitedFu)
}
)
else

View File

@ -36,7 +36,7 @@ final class Export(env: Env) extends LilaController(env) {
stream("image/gif") map
gameImageCacheSeconds(game)
}
}
}(rateLimitedFu)
}
}
@ -53,7 +53,7 @@ final class Export(env: Env) extends LilaController(env) {
stream("image/gif") map
gameImageCacheSeconds(game)
}
}
}(rateLimitedFu)
}
def legacyPuzzleThumbnail(id: Int) =
@ -73,7 +73,7 @@ final class Export(env: Env) extends LilaController(env) {
res.withHeaders(CACHE_CONTROL -> "max-age=86400")
}
}
}
}(rateLimitedFu)
}
private def gameImageCacheSeconds(game: lila.game.Game)(res: Result): Result = {

View File

@ -42,7 +42,7 @@ final class ForumPost(env: Env) extends LilaController(env) with ForumController
postApi.makePost(categ, topic, data) map { post =>
Redirect(routes.ForumPost.redirect(post.id))
}
}
}(rateLimitedFu)
)
}
}
@ -58,7 +58,7 @@ final class ForumPost(env: Env) extends LilaController(env) with ForumController
postApi.editPost(postId, data.changes, me).map { post =>
Redirect(routes.ForumPost.redirect(post.id))
}
}
}(rateLimitedFu)
)
}

View File

@ -36,7 +36,7 @@ final class ForumTopic(env: Env) extends LilaController(env) with ForumControlle
topicApi.makeTopic(categ, data, me) map { topic =>
Redirect(routes.ForumTopic.show(categ.slug, topic.slug, 1))
}
}
}(rateLimitedFu)
)
}
}

View File

@ -47,6 +47,8 @@ abstract private[controllers] class LilaController(val env: Env)
protected val keyPages = new KeyPages(env)
protected val renderNotFound = keyPages.notFound _
protected val rateLimited = Results.TooManyRequests
protected val rateLimitedFu = rateLimited.fuccess
implicit protected def LilaFunitToResult(
@nowarn("cat=unused") funit: Funit

View File

@ -190,7 +190,7 @@ final class Plan(env: Env)(implicit system: akka.actor.ActorSystem) extends Lila
.flatMap(customer => createStripeSession(checkout, customer.id))
}
)
}
}(rateLimitedFu)
}
def payPalIpn =

View File

@ -71,7 +71,7 @@ final class Search(env: Env) extends LilaController(env) {
)
)
}
}
}(rateLimitedFu)
}
}
}

View File

@ -133,7 +133,7 @@ final class Setup(
}
}
)
}
}(rateLimitedFu)
}
def hookForm =
@ -184,7 +184,7 @@ final class Setup(
}
)
}
}
}(rateLimitedFu)
}
}
@ -204,7 +204,7 @@ final class Setup(
.hook(hookConfig, Sri(sri), HTTPRequest sid ctx.req, blocking ++ sameOpponents)
} yield hookResponse(hookResult)
}
}
}(rateLimitedFu)
}
}
@ -230,7 +230,7 @@ final class Setup(
BoardApiHookConcurrencyLimitPerUser(me.id)(
env.lobby.boardApiHookStream(hook.copy(boardApi = true))
)(apiC.sourceToNdJsonOption).fuccess
}
}(rateLimitedFu)
case _ => BadRequest(jsonError("Invalid board API seek")).fuccess
}
}
@ -272,7 +272,7 @@ final class Setup(
Created(env.game.jsonView(pov.game, config.fen)) as JSON
}
)
}
}(rateLimitedFu)
}
private def process[A](form: Context => Form[A])(op: A => BodyContext[_] => Fu[Pov]) =
@ -296,7 +296,7 @@ final class Setup(
)
}
)
}
}(rateLimitedFu)
}
private[controllers] def redirectPov(pov: Pov)(implicit ctx: Context) = {

View File

@ -412,8 +412,8 @@ final class Study(
}
}
}
}
}
}(rateLimitedFu)
}(rateLimitedFu)
}
private val PgnRateLimitPerIp = new lila.memo.RateLimit[IpAddress](
@ -438,7 +438,7 @@ final class Study(
.fuccess
}
}
}
}(rateLimitedFu)
}
def chapterPgn(id: String, chapterId: String) =

View File

@ -430,7 +430,7 @@ final class Team(
You received this message because you are part of the team lichess.org${routes.Team.show(team.id)}."""
env.msg.api.multiPost(me, env.team.memberStream.ids(team, MaxPerSecond(50)), full)
funit // we don't wait for the stream to complete, it would make lichess time out
}
}(funit)
}
)

View File

@ -250,9 +250,7 @@ final class Tournament(
key = "tournament.ip"
)
private val rateLimited = ornicar.scalalib.Zero.instance[Fu[Result]] {
fuccess(Redirect(routes.Tournament.home))
}
private val rateLimitedCreation = fuccess(Redirect(routes.Tournament.home))
private[controllers] def rateLimitCreation(me: UserModel, isPrivate: Boolean, req: RequestHeader)(
create: => Fu[Result]
@ -270,8 +268,8 @@ final class Tournament(
CreateLimitPerUser(me.id, cost = cost) {
CreateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = cost) {
create
}(rateLimited)
}(rateLimited)
}(rateLimitedCreation)
}(rateLimitedCreation)
}
def create =

View File

@ -217,9 +217,6 @@ final class User(
key = "user_games.web.ip"
)
implicit val userGamesDefault =
ornicar.scalalib.Zero.instance[Fu[Paginator[GameModel]]](fuccess(Paginator.empty[GameModel]))
private def userGames(
u: UserModel,
filterName: String,
@ -241,7 +238,7 @@ final class User(
}
_ <- env.user.lightUserApi preloadMany pag.currentPageResults.flatMap(_.userIds)
} yield pag
}
}(fuccess(Paginator.empty[GameModel]))
}
def list =

View File

@ -11,7 +11,7 @@ final class HttpFilter(env: Env)(implicit val mat: Materializer) extends Filter
private val httpMon = lila.mon.http
private val net = env.net
private val logger = lila.log("http")
private val logRequests = false
private val logRequests = true
def apply(nextFilter: RequestHeader => Fu[Result])(req: RequestHeader): Fu[Result] =
if (HTTPRequest isAssets req) nextFilter(req) dmap { result =>
@ -34,11 +34,10 @@ final class HttpFilter(env: Env)(implicit val mat: Materializer) extends Filter
val actionName = HTTPRequest actionName req
val reqTime = nowMillis - startTime
val statusCode = result.header.status
if (env.isDev && logRequests) logger.info(s"$statusCode $req $actionName ${reqTime}ms")
else {
val client = HTTPRequest clientName req
httpMon.time(actionName, client, req.method, statusCode).record(reqTime)
}
val client = HTTPRequest clientName req
if (env.isDev && logRequests)
logger.info(s"$statusCode $client $req ${req.method} $actionName ${reqTime}ms")
else httpMon.time(actionName, client, req.method, statusCode).record(reqTime)
}
private def redirectWrongDomain(req: RequestHeader): Option[Result] =

View File

@ -50,7 +50,7 @@ final private class Limiter(
case Work.Sender(Some(userId), _, _, _) => requesterApi.countToday(userId) map (_ < maxPerDay)
case Work.Sender(_, Some(ip), _, _) =>
fuccess {
RequestLimitPerIP(ip, cost = 1)(true)
RequestLimitPerIP(ip, cost = 1)(true)(false)
}
case _ => fuFalse
}

View File

@ -1,6 +1,5 @@
package lila.lobby
import ornicar.scalalib.Zero
import play.api.libs.json._
import scala.concurrent.duration._
import scala.concurrent.Promise
@ -153,8 +152,8 @@ final class LobbySocket(
key = "lobby.hook_pool.member"
)
private def HookPoolLimit[A: Zero](member: Member, cost: Int, msg: => String)(op: => A) =
poolLimitPerSri(k = member.sri.value, cost = cost, msg = msg)(op)
private def HookPoolLimit(member: Member, cost: Int, msg: => String)(op: => Unit) =
poolLimitPerSri(k = member.sri.value, cost = cost, msg = msg)(op) {}
def controller(member: Member): SocketController = {
case ("join", o) if !member.bot =>

View File

@ -27,10 +27,10 @@ final class RateLimit[K](
def chargeable[A](k: K, cost: Cost = 1, msg: => String = "")(
op: Charge => A
)(implicit default: Zero[A]): A =
apply(k, cost, msg) { op(c => apply(k, c, s"charge: $msg")(())) }
)(default: => A): A =
apply(k, cost, msg) { op(c => apply(k, c, s"charge: $msg") {} {}) }(default)
def apply[A](k: K, cost: Cost = 1, msg: => String = "")(op: => A)(implicit default: Zero[A]): A =
def apply[A](k: K, cost: Cost = 1, msg: => String = "")(op: => A)(default: => A): A =
storage getIfPresent k match {
case None =>
storage.put(k, cost -> makeClearAt)
@ -44,7 +44,7 @@ final class RateLimit[K](
case _ if enforce =>
if (log) logger.info(s"$credits/$duration $k cost: $cost $msg")
monitor.increment()
default.zero
default
case _ =>
op
}
@ -57,11 +57,9 @@ object RateLimit {
trait RateLimiter[K] {
def apply[A](k: K, cost: Cost = 1, msg: => String = "")(op: => A)(implicit default: Zero[A]): A
def apply[A](k: K, cost: Cost = 1, msg: => String = "")(op: => A)(default: => A): A
def chargeable[A](k: K, cost: Cost = 1, msg: => String = "")(op: Charge => A)(
implicit default: Zero[A]
): A
def chargeable[A](k: K, cost: Cost = 1, msg: => String = "")(op: Charge => A)(default: => A): A
}
def composite[K](
@ -85,18 +83,16 @@ object RateLimit {
new RateLimiter[K] {
def apply[A](k: K, cost: Cost = 1, msg: => String = "")(op: => A)(implicit default: Zero[A]): A = {
def apply[A](k: K, cost: Cost = 1, msg: => String = "")(op: => A)(default: => A): A = {
val accepted = limiters.foldLeft(true) {
case (true, limiter) => limiter(k, cost, msg)(true)
case (true, limiter) => limiter(k, cost, msg)(true)(false)
case (false, _) => false
}
if (accepted) op else default.zero
if (accepted) op else default
}
def chargeable[A](k: K, cost: Cost = 1, msg: => String = "")(op: Charge => A)(
implicit default: Zero[A]
): A = {
apply(k, cost, msg) { op(c => apply(k, c, s"charge: $msg")(())) }
def chargeable[A](k: K, cost: Cost = 1, msg: => String = "")(op: Charge => A)(default: => A): A = {
apply(k, cost, msg) { op(c => apply(k, c, s"charge: $msg") {} {}) }(default)
}
}
}

View File

@ -70,7 +70,7 @@ final private class MsgSecurity(
else {
val limiter = if (isNew) CreateLimitPerUser else ReplyLimitPerUser
val cost = if (user.isVerified) 1 else 5
!limiter(user.id, cost = cost)(true) ?? fuccess(Limit.some)
!limiter(user.id, cost = cost)(true)(false) ?? fuccess(Limit.some)
}
private def isSpam(text: String): Fu[Option[Verdict]] =

View File

@ -166,7 +166,7 @@ final class RelationApi(
(config.maxFollow < nb) ?? {
limitFollowRateLimiter(u) {
fetchFollowing(u) flatMap userRepo.filterClosedOrInactiveIds(DateTime.now.minusDays(90))
} flatMap {
}(fuccess(Nil)) flatMap {
case Nil => repo.drop(u, true, nb - config.maxFollow.value)
case inactiveIds =>
repo.unfollowMany(u, inactiveIds) >>-

View File

@ -49,7 +49,8 @@ final private class Rematcher(
case Pov(game, color) if game.playerCouldRematch =>
if (isOffering(!pov) || game.opponent(color).isAi)
rematches.of(game.id).fold(rematchJoin(pov))(rematchExists(pov))
else if (!declined.get(pov.flip.fullId) && rateLimit(pov.fullId)(true)) fuccess(rematchCreate(pov))
else if (!declined.get(pov.flip.fullId) && rateLimit(pov.fullId)(true)(false))
fuccess(rematchCreate(pov))
else fuccess(List(Event.RematchOffer(by = none)))
case _ => fuccess(List(Event.ReloadOwner))
}

View File

@ -148,14 +148,14 @@ object EmailConfirm {
key = "email.confirms.email"
)
def rateLimit[A: Zero](userEmail: UserEmail, req: RequestHeader)(run: => Fu[A]): Fu[A] =
def rateLimit[A: Zero](userEmail: UserEmail, req: RequestHeader)(run: => Fu[A])(default: => Fu[A]): Fu[A] =
rateLimitPerUser(userEmail.username, cost = 1) {
rateLimitPerEmail(userEmail.email.value, cost = 1) {
rateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = 1) {
run
}
}
}
}(default)
}(default)
}(default)
object Help {

View File

@ -77,12 +77,14 @@ object MagicLink {
key = "email.confirms.email"
)
def rateLimit[A: Zero](user: User, email: EmailAddress, req: RequestHeader)(run: => Fu[A]): Fu[A] =
def rateLimit[A: Zero](user: User, email: EmailAddress, req: RequestHeader)(
run: => Fu[A]
)(default: => Fu[A]): Fu[A] =
rateLimitPerUser(user.id, cost = 1) {
rateLimitPerEmail(email.value, cost = 1) {
rateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = 1) {
run
}
}
}
}(default)
}(default)
}(default)
}

View File

@ -65,7 +65,7 @@ final class Signup(
authLog(data.username, data.email, "Signup recaptcha fail")
fuccess(Signup.Bad(forms.signup.website fill data))
case true =>
rateLimit(data.username, if (data.recaptchaResponse.isDefined) 1 else 2) {
signupRateLimit(data.username, if (data.recaptchaResponse.isDefined) 1 else 2) {
MustConfirmEmail(data.fingerPrint) flatMap {
mustConfirm =>
lila.mon.user.register.count(none)
@ -113,7 +113,7 @@ final class Signup(
forms.signup.mobile.bindFromRequest.fold[Fu[Signup.Result]](
err => fuccess(Signup.Bad(err tap signupErrLog)),
data =>
rateLimit(data.username, cost = 2) {
signupRateLimit(data.username, cost = 2) {
val email = emailAddressValidator
.validate(data.realEmail) err s"Invalid email ${data.email}"
val mustConfirm = MustConfirmEmail.YesBecauseMobile
@ -137,12 +137,10 @@ final class Signup(
}
)
implicit private val ResultZero = ornicar.scalalib.Zero.instance[Signup.Result](Signup.RateLimited)
private def HasherRateLimit =
PasswordHasher.rateLimit[Signup.Result](enforce = netConfig.rateLimit) _
private lazy val rateLimitPerIP = RateLimit.composite[IpAddress](
private lazy val signupRateLimitPerIP = RateLimit.composite[IpAddress](
name = "Accounts per IP",
key = "account.create.ip",
enforce = netConfig.rateLimit.value
@ -151,12 +149,14 @@ final class Signup(
("slow", 150, 1 day)
)
private def rateLimit(username: String, cost: Int)(
private val rateLimitDefault = fuccess(Signup.RateLimited)
private def signupRateLimit(username: String, cost: Int)(
f: => Fu[Signup.Result]
)(implicit req: RequestHeader): Fu[Signup.Result] =
HasherRateLimit(username, req) { _ =>
rateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = cost)(f)
}
signupRateLimitPerIP(HTTPRequest lastRemoteAddress req, cost = cost)(f)(rateLimitDefault)
}(rateLimitDefault)
private def logSignup(
req: RequestHeader,

View File

@ -51,7 +51,7 @@ final class Env(
.buildAsyncFuture(_ => repo.allCreatedFeaturable)
}
val featurable = new SimulIsFeaturable((simul: Simul) => featureLimiter(simul.hostId)(true))
val featurable = new SimulIsFeaturable((simul: Simul) => featureLimiter(simul.hostId)(true)(false))
private val featureLimiter = new lila.memo.RateLimit[lila.user.User.ID](
credits = config.featureViews.value,

View File

@ -151,7 +151,7 @@ final class SlackApi(
channel = rooms.tavernBots
)
)
}
}(funit)
def commReportBurst(user: User): Funit =
client(

View File

@ -38,5 +38,5 @@ final private class SlackClient(ws: WSClient, url: Secret)(implicit ec: scala.co
case res if res.status == 200 => funit
case res => fufail(s"[slack] $url $msg ${res.status} ${res.body}")
}
}
}(funit)
}

View File

@ -70,7 +70,7 @@ final private class StudyInvite(
)
val notification = Notification.make(Notification.Notifies(invited.id), notificationContent)
notifyApi.addNotification(notification)
}
}(funit)
} yield ()
def admin(study: Study, user: User): Funit =

View File

@ -206,7 +206,7 @@ final private class StudySocket(
isPresent = userId => isPresent(studyId, userId),
onError = err => send(P.Out.tellSri(w.sri, makeMessage("error", err)))
)
}
}(funit)
case "relaySync" =>
who foreach { w =>
Bus.publish(actorApi.RelayToggle(studyId, ~(o \ "d").asOpt[Boolean], w), "relayToggle")

View File

@ -78,7 +78,6 @@ object PasswordHasher {
import scala.concurrent.duration._
import play.api.mvc.RequestHeader
import ornicar.scalalib.Zero
import lila.memo.RateLimit
import lila.common.{ HTTPRequest, IpAddress }
@ -110,9 +109,9 @@ object PasswordHasher {
key = "password.hashes.global"
)
def rateLimit[A: Zero](
def rateLimit[A](
enforce: lila.common.config.RateLimit
)(username: String, req: RequestHeader)(run: RateLimit.Charge => Fu[A]): Fu[A] =
)(username: String, req: RequestHeader)(run: RateLimit.Charge => Fu[A])(default: => Fu[A]): Fu[A] =
if (enforce.value) {
val cost = 1
val ip = HTTPRequest lastRemoteAddress req
@ -121,9 +120,9 @@ object PasswordHasher {
rateLimitPerUA(~HTTPRequest.userAgent(req), cost = cost, msg = ip.value) {
rateLimitGlobal("-", cost = cost, msg = ip.value) {
run(charge)
}
}
}
}
}(default)
}(default)
}(default)
}(default)
} else run(_ => ())
}