type safe IP addresses

practice-feedback
Thibault Duplessis 2017-02-16 01:53:15 +01:00
parent 560b5af4c1
commit cd0fed508c
42 changed files with 194 additions and 130 deletions

View File

@ -6,7 +6,7 @@ import scala.concurrent.duration._
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.common.{ HTTPRequest, IpAddress }
object Api extends LilaController {
@ -39,14 +39,14 @@ object Api extends LilaController {
userApi one name map toApiResult
}
private val UsersRateLimitGlobal = new lila.memo.RateLimit(
private val UsersRateLimitGlobal = new lila.memo.RateLimit[String](
credits = 1000,
duration = 1 minute,
name = "team users API global",
key = "team_users.api.global"
)
private val UsersRateLimitPerIP = new lila.memo.RateLimit(
private val UsersRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 1000,
duration = 10 minutes,
name = "team users API per IP",
@ -59,7 +59,7 @@ object Api extends LilaController {
val cost = page * nb + 10
val ip = HTTPRequest lastRemoteAddress ctx.req
UsersRateLimitPerIP(ip, cost = cost) {
UsersRateLimitGlobal("-", cost = cost, msg = ip) {
UsersRateLimitGlobal("-", cost = cost, msg = ip.value) {
lila.mon.api.teamUsers.cost(cost)
(get("team") ?? Env.team.api.team).flatMap {
_ ?? { team =>
@ -75,7 +75,7 @@ object Api extends LilaController {
val ip = HTTPRequest lastRemoteAddress ctx.req
val cost = usernames.size / 4
UsersRateLimitPerIP(ip, cost = cost) {
UsersRateLimitGlobal("-", cost = cost, msg = ip) {
UsersRateLimitGlobal("-", cost = cost, msg = ip.value) {
lila.mon.api.users.cost(1)
lila.user.UserRepo nameds usernames map {
_.map { Env.user.jsonView(_, none) }
@ -101,21 +101,21 @@ object Api extends LilaController {
}
}
private val UserGamesRateLimitPerIP = new lila.memo.RateLimit(
private val UserGamesRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 10 * 1000,
duration = 10 minutes,
name = "user games API per IP",
key = "user_games.api.ip"
)
private val UserGamesRateLimitPerUA = new lila.memo.RateLimit(
private val UserGamesRateLimitPerUA = new lila.memo.RateLimit[String](
credits = 10 * 1000,
duration = 5 minutes,
name = "user games API per UA",
key = "user_games.api.ua"
)
private val UserGamesRateLimitGlobal = new lila.memo.RateLimit(
private val UserGamesRateLimitGlobal = new lila.memo.RateLimit[String](
credits = 10 * 1000,
duration = 1 minute,
name = "user games API global",
@ -128,8 +128,8 @@ object Api extends LilaController {
val cost = page * nb + 10
val ip = HTTPRequest lastRemoteAddress ctx.req
UserGamesRateLimitPerIP(ip, cost = cost) {
UserGamesRateLimitPerUA(~HTTPRequest.userAgent(ctx.req), cost = cost, msg = ip) {
UserGamesRateLimitGlobal("-", cost = cost, msg = ip) {
UserGamesRateLimitPerUA(~HTTPRequest.userAgent(ctx.req), cost = cost, msg = ip.value) {
UserGamesRateLimitGlobal("-", cost = cost, msg = ip.value) {
lila.mon.api.userGames.cost(cost)
lila.user.UserRepo named name flatMap {
_ ?? { user =>
@ -153,7 +153,7 @@ object Api extends LilaController {
}
}
private val GameRateLimitPerIP = new lila.memo.RateLimit(
private val GameRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 100,
duration = 1 minute,
name = "game API per IP",

View File

@ -7,7 +7,7 @@ import play.api.Play.current
import lila.api.Context
import lila.app._
import lila.common.{ LilaCookie, HTTPRequest }
import lila.common.{ LilaCookie, HTTPRequest, IpAddress }
import lila.user.{ UserRepo, User => UserModel }
import views._
@ -98,7 +98,7 @@ object Auth extends LilaController {
}
}
private def mustConfirmEmailByIP(ip: String, username: String): Fu[Boolean] =
private def mustConfirmEmailByIP(ip: IpAddress, username: String): Fu[Boolean] =
fuccess(username.toLowerCase.contains("argeskent")) >>|
api.recentByIpExists(ip) >>|
Mod.ipIntelCache.get(ip).map(80 <).recover { case _: Exception => false }

View File

@ -40,7 +40,7 @@ object Export extends LilaController {
})
}
private val PngRateLimitGlobal = new lila.memo.RateLimit(
private val PngRateLimitGlobal = new lila.memo.RateLimit[String](
credits = 60,
duration = 1 minute,
name = "export PGN global",
@ -49,7 +49,7 @@ object Export extends LilaController {
def png(id: String) = Open { implicit ctx =>
OnlyHumansAndFacebookOrTwitter {
PngRateLimitGlobal("-", msg = HTTPRequest lastRemoteAddress ctx.req) {
PngRateLimitGlobal("-", msg = HTTPRequest.lastRemoteAddress(ctx.req).value) {
lila.mon.export.png.game()
OptionFuResult(GameRepo game id) { game =>
env.pngExport fromGame game map { stream =>
@ -65,7 +65,7 @@ object Export extends LilaController {
def puzzlePng(id: Int) = Open { implicit ctx =>
OnlyHumansAndFacebookOrTwitter {
PngRateLimitGlobal("-", msg = HTTPRequest lastRemoteAddress ctx.req) {
PngRateLimitGlobal("-", msg = HTTPRequest.lastRemoteAddress(ctx.req).value) {
lila.mon.export.png.puzzle()
OptionFuResult(Env.puzzle.api.puzzle find id) { puzzle =>
env.pngExport(

View File

@ -67,7 +67,7 @@ object Fishnet extends LilaController {
logger.warn(s"Malformed request: $err\n${req.body}")
BadRequest(jsonError(JsError toJson err)).fuccess
},
data => api.authenticateClient(data, clientIp(req)) flatMap {
data => api.authenticateClient(data, HTTPRequest lastRemoteAddress req) flatMap {
case Failure(msg) => {
val ip = HTTPRequest.lastRemoteAddress(req)
logger.info(s"Unauthorized key: ${data.fishnet.apikey} ip: $ip | ${msg.getMessage}")
@ -81,8 +81,4 @@ object Fishnet extends LilaController {
}
)
}
private def clientIp(req: RequestHeader) = lila.fishnet.Client.IpAddress {
HTTPRequest lastRemoteAddress req
}
}

View File

@ -1,13 +1,13 @@
package controllers
import lila.app._
import lila.common.HTTPRequest
import lila.common.{ HTTPRequest, IpAddress }
import scala.concurrent.duration._
import views._
object ForumPost extends LilaController with ForumController {
private val CreateRateLimit = new lila.memo.RateLimit(4, 5 minutes,
private val CreateRateLimit = new lila.memo.RateLimit[IpAddress](4, 5 minutes,
name = "forum create post",
key = "forum.post")

View File

@ -3,14 +3,14 @@ package controllers
import scala.concurrent.duration._
import lila.app._
import lila.common.HTTPRequest
import lila.common.{ HTTPRequest, IpAddress }
import lila.forum.CategRepo
import play.api.libs.json._
import views._
object ForumTopic extends LilaController with ForumController {
private val CreateRateLimit = new lila.memo.RateLimit(2, 5 minutes,
private val CreateRateLimit = new lila.memo.RateLimit[IpAddress](2, 5 minutes,
name = "forum create topic",
key = "forum.topic")

View File

@ -6,7 +6,7 @@ import play.api.mvc.WebSocket.FrameFormatter
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.common.{ HTTPRequest, IpAddress }
trait LilaSocket { self: LilaController =>
@ -31,7 +31,7 @@ trait LilaSocket { self: LilaController =>
f(ctx).map(_ toRight notFoundResponse)
}
protected def SocketOptionLimited[A: FrameFormatter](limiter: lila.memo.RateLimit, name: String)(f: Context => Fu[Option[Pipe[A]]]) =
protected def SocketOptionLimited[A: FrameFormatter](limiter: lila.memo.RateLimit[IpAddress], name: String)(f: Context => Fu[Option[Pipe[A]]]) =
rateLimitedSocket[A](limiter, name) { ctx =>
f(ctx).map(_ toRight notFoundResponse)
}
@ -40,7 +40,7 @@ trait LilaSocket { self: LilaController =>
private val rateLimitLogger = lila.log("ratelimit")
private def rateLimitedSocket[A: FrameFormatter](limiter: lila.memo.RateLimit, name: String)(f: AcceptType[A]): WebSocket[A, A] =
private def rateLimitedSocket[A: FrameFormatter](limiter: lila.memo.RateLimit[IpAddress], name: String)(f: AcceptType[A]): WebSocket[A, A] =
WebSocket[A, A] { req =>
SocketCSRF(req) {
reqToCtx(req) flatMap { ctx =>

View File

@ -5,6 +5,7 @@ import play.api.mvc._
import play.twirl.api.Html
import scala.concurrent.duration._
import lila.common.IpAddress
import lila.api.Context
import lila.app._
import views._
@ -42,7 +43,7 @@ object Lobby extends LilaController {
)
}
private val MessageLimitPerIP = new lila.memo.RateLimit(
private val MessageLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 40,
duration = 10 seconds,
name = "lobby socket message per IP",

View File

@ -90,28 +90,12 @@ object Main extends LilaController {
}
def jslog(id: String) = Open { ctx =>
val known = ctx.me.??(_.engine)
val referer = HTTPRequest.referer(ctx.req)
val name = get("n", ctx.req) | "?"
lila.mon.cheat.cssBot()
ctx.userId.ifTrue(!known && name != "ceval") ?? {
Env.report.api.autoBotReport(_, referer, name)
}
def doLog = lila.log("cheat").branch("jslog").info(
s"${ctx.req.remoteAddress} ${referer | "?"} ${ctx.userId | "anon"} $name"
Env.round.selfReport(
userId = ctx.userId,
ip = HTTPRequest lastRemoteAddress ctx.req,
fullId = id,
name = get("n", ctx.req) | "?"
)
if (id == "________") doLog
else lila.game.GameRepo pov id foreach {
_ foreach { pov =>
if (!known) doLog
if (name == "ceval" || name == "rcb" || name == "ccs")
Env.round.roundMap ! lila.hub.actorApi.map.Tell(
pov.gameId,
lila.round.actorApi.round.Cheat(pov.color)
)
else lila.game.GameRepo.setBorderAlert(pov)
}
}
Ok.fuccess
}

View File

@ -3,6 +3,7 @@ package controllers
import lila.api.Context
import lila.app._
import lila.user.{ UserRepo, User => UserModel }
import lila.common.IpAddress
import views._
import scala.concurrent.duration._
@ -153,7 +154,7 @@ object Mod extends LilaController {
}
private[controllers] val ipIntelCache =
Env.memo.asyncCache.multi[String, Int](
Env.memo.asyncCache.multi[IpAddress, Int](
name = "ipIntel",
f = ip => {
import play.api.libs.ws.WS
@ -177,7 +178,7 @@ object Mod extends LilaController {
)
def ipIntel(ip: String) = Secure(_.IpBan) { ctx => me =>
ipIntelCache.get(ip).map { Ok(_) }.recover {
ipIntelCache.get(IpAddress(ip)).map { Ok(_) }.recover {
case e: Exception => InternalServerError(e.getMessage)
}
}

View File

@ -150,7 +150,7 @@ object Plan extends LilaController {
cents = lila.plan.Cents(ipn.grossCents),
name = ipn.name,
txnId = ipn.txnId,
ip = lila.common.HTTPRequest.lastRemoteAddress(req),
ip = lila.common.HTTPRequest.lastRemoteAddress(req).value,
key = lila.plan.PayPalIpnKey(get("key", req) | "N/A")
) inject Ok
)

View File

@ -3,7 +3,7 @@ package controllers
import scala.concurrent.duration._
import lila.app._
import lila.common.HTTPRequest
import lila.common.{ HTTPRequest, IpAddress }
import views._
object Search extends LilaController {
@ -11,14 +11,14 @@ object Search extends LilaController {
private def env = Env.gameSearch
def searchForm = env.forms.search
private val RateLimitGlobal = new lila.memo.RateLimit(
private val RateLimitGlobal = new lila.memo.RateLimit[String](
credits = 50,
duration = 1 minute,
name = "search games global",
key = "search.games.global"
)
private val RateLimitPerIP = new lila.memo.RateLimit(
private val RateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 50,
duration = 5 minutes,
name = "search games per IP",

View File

@ -9,7 +9,7 @@ import scala.concurrent.duration._
import lila.api.{ Context, BodyContext }
import lila.app._
import lila.common.{ HTTPRequest, LilaCookie }
import lila.common.{ HTTPRequest, LilaCookie, IpAddress }
import lila.game.{ GameRepo, Pov, AnonCookie }
import lila.setup.Processor.HookResult
import lila.setup.ValidFen
@ -20,7 +20,7 @@ object Setup extends LilaController with TheftPrevention {
private def env = Env.setup
private val PostRateLimit = new lila.memo.RateLimit(5, 1 minute,
private val PostRateLimit = new lila.memo.RateLimit[IpAddress](5, 1 minute,
name = "setup post",
key = "setup_post")

View File

@ -6,7 +6,7 @@ import scala.concurrent.duration._
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.common.{ IpAddress, HTTPRequest }
import lila.study.Study.WithChapter
import lila.study.{ Chapter, Order, Study => StudyModel }
import views._
@ -212,14 +212,14 @@ object Study extends LilaController {
}
}
private val CloneLimitPerUser = new lila.memo.RateLimit(
private val CloneLimitPerUser = new lila.memo.RateLimit[lila.user.User.ID](
credits = 10 * 3,
duration = 24 hour,
name = "clone study per user",
key = "clone_study.user"
)
private val CloneLimitPerIP = new lila.memo.RateLimit(
private val CloneLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 20 * 3,
duration = 24 hour,
name = "clone study per IP",
@ -242,7 +242,7 @@ object Study extends LilaController {
}
}
private val PgnRateLimitGlobal = new lila.memo.RateLimit(
private val PgnRateLimitGlobal = new lila.memo.RateLimit[String](
credits = 30,
duration = 1 minute,
name = "export study PGN global",
@ -251,7 +251,7 @@ object Study extends LilaController {
def pgn(id: String) = Open { implicit ctx =>
OnlyHumans {
PgnRateLimitGlobal("-", msg = HTTPRequest lastRemoteAddress ctx.req) {
PgnRateLimitGlobal("-", msg = HTTPRequest.lastRemoteAddress(ctx.req).value) {
OptionFuResult(env.api byId id) { study =>
CanViewResult(study) {
lila.mon.export.pgn.study()

View File

@ -7,7 +7,7 @@ import scala.concurrent.duration._
import lila.api.BodyContext
import lila.app._
import lila.app.mashup.GameFilterMenu
import lila.common.HTTPRequest
import lila.common.{ IpAddress, HTTPRequest }
import lila.common.paginator.Paginator
import lila.game.{ GameRepo, Game => GameModel }
import lila.rating.PerfType
@ -136,7 +136,7 @@ object User extends LilaController {
searchForm = GameFilterMenu.searchForm(userGameSearch, filters.current)(ctx.body)
} yield html.user.show(u, info, pag, filters, searchForm, relation, notes, followable, blocked)
private val UserGamesRateLimitPerIP = new lila.memo.RateLimit(
private val UserGamesRateLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 500,
duration = 10 minutes,
name = "user games web/mobile per IP",

View File

@ -32,8 +32,9 @@ object HTTPRequest {
def referer(req: RequestHeader): Option[String] = req.headers get HeaderNames.REFERER
def lastRemoteAddress(req: RequestHeader): String =
def lastRemoteAddress(req: RequestHeader) = IpAddress {
req.remoteAddress.split(", ").lastOption | req.remoteAddress
}
def sid(req: RequestHeader): Option[String] = req.session get LilaCookie.sessionId

View File

@ -24,4 +24,6 @@ object Iso {
implicit def isoIdentity[A]: Iso[A, A] = apply(identity[A] _, identity[A] _)
implicit val stringIsoIdentity: Iso[String, String] = isoIdentity[String]
implicit val ipAddressIso = string[IpAddress](IpAddress.apply, _.value)
}

View File

@ -8,3 +8,15 @@ case class ApiVersion(value: Int) extends AnyVal with IntValue {
case class AssetVersion(value: Int) extends AnyVal with IntValue
case class MaxPerPage(value: Int) extends AnyVal with IntValue
case class IpAddress(value: String) extends AnyVal with StringValue
object IpAddress {
// http://stackoverflow.com/questions/106179/regular-expression-to-match-hostname-or-ip-address
private val ipv4Regex = """^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$""".r
// ipv6 address in standard form (no compression, no leading zeros)
private val ipv6Regex = """^((0|[1-9a-f][0-9a-f]{0,3}):){7}(0|[1-9a-f][0-9a-f]{0,3})""".r
def isv4(a: IpAddress) = ipv4Regex matches a.value
def isv6(a: IpAddress) = ipv6Regex matches a.value
}

View File

@ -4,8 +4,8 @@ import org.joda.time.DateTime
import reactivemongo.bson._
import scalaz.NonEmptyList
import lila.common.Iso
import lila.common.Iso._
import lila.common.{ Iso, IpAddress }
trait Handlers {
@ -58,4 +58,6 @@ trait Handlers {
reader.read(v.get)
}
}
implicit val ipAddressHandler = isoHandler[IpAddress, String, BSONString](ipAddressIso)
}

View File

@ -1,6 +1,7 @@
package lila.fishnet
import lila.db.BSON.{ BSONJodaDateTimeHandler, stringAnyValHandler }
import lila.common.IpAddress
import reactivemongo.bson._
import chess.format.FEN
@ -12,7 +13,7 @@ private object BSONHandlers {
implicit val ClientVersionBSONHandler = stringAnyValHandler[Client.Version](_.value, Client.Version.apply)
implicit val ClientPythonBSONHandler = stringAnyValHandler[Client.Python](_.value, Client.Python.apply)
implicit val ClientUserIdBSONHandler = stringAnyValHandler[Client.UserId](_.value, Client.UserId.apply)
implicit val ClientIpAddressBSONHandler = stringAnyValHandler[Client.IpAddress](_.value, Client.IpAddress.apply)
implicit val ClientIpAddressBSONHandler = stringAnyValHandler[IpAddress](_.value, IpAddress.apply)
implicit val ClientSkillBSONHandler = new BSONHandler[BSONString, Client.Skill] {
def read(x: BSONString) = Client.Skill byKey x.value err s"Invalid client skill ${x.value}"

View File

@ -1,5 +1,7 @@
package lila.fishnet
import lila.common.IpAddress
import org.joda.time.DateTime
case class Client(
@ -42,7 +44,6 @@ object Client {
case class Version(value: String) extends AnyVal with StringValue
case class Python(value: String) extends AnyVal with StringValue
case class UserId(value: String) extends AnyVal with StringValue
case class IpAddress(value: String) extends AnyVal with StringValue
case class Engine(name: String)
case class Engines(stockfish: Engine)

View File

@ -4,6 +4,7 @@ import org.joda.time.DateTime
import reactivemongo.bson._
import scala.util.{ Try, Success, Failure }
import lila.common.IpAddress
import Client.Skill
import lila.db.dsl._
import lila.hub.FutureSequencer
@ -27,7 +28,7 @@ final class FishnetApi(
def keyExists(key: Client.Key) = repo.getEnabledClient(key).map(_.isDefined)
def authenticateClient(req: JsonApi.Request, ip: Client.IpAddress): Fu[Try[Client]] = {
def authenticateClient(req: JsonApi.Request, ip: IpAddress): Fu[Try[Client]] = {
if (offlineMode) repo.getOfflineClient map some
else repo.getEnabledClient(req.fishnet.apikey)
} map {

View File

@ -6,10 +6,10 @@ import play.api.libs.json._
import chess.format.{ Uci, FEN }
import chess.variant.Variant
import lila.common.Maths
import lila.common.{ Maths, IpAddress }
import lila.fishnet.{ Work => W }
import lila.tree.Eval.{ Cp, Mate }
import lila.tree.Eval.JsonHandlers._
import lila.tree.Eval.{ Cp, Mate }
object JsonApi {
@ -17,7 +17,7 @@ object JsonApi {
val fishnet: Request.Fishnet
val stockfish: Request.Engine
def instance(ip: Client.IpAddress) = Client.Instance(
def instance(ip: IpAddress) = Client.Instance(
fishnet.version,
fishnet.python | Client.Python(""),
Client.Engines(

View File

@ -3,7 +3,8 @@ package lila.fishnet
import scala.concurrent.duration._
import reactivemongo.bson._
import lila.db.dsl.Coll
import lila.common.IpAddress
import lila.db.dsl._
private final class Limiter(
analysisColl: Coll,
@ -16,7 +17,7 @@ private final class Limiter(
case true => perDayCheck(sender)
}
private val RequestLimitPerIP = new lila.memo.RateLimit(
private val RequestLimitPerIP = new lila.memo.RateLimit[IpAddress](
credits = 50,
duration = 20 hours,
name = "request analysis per IP",
@ -25,12 +26,12 @@ private final class Limiter(
private def concurrentCheck(sender: Work.Sender) = sender match {
case Work.Sender(_, _, mod, system) if (mod || system) => fuccess(true)
case Work.Sender(Some(userId), _, _, _) => analysisColl.count(BSONDocument(
case Work.Sender(Some(userId), _, _, _) => !analysisColl.exists($doc(
"sender.userId" -> userId
).some) map (0 ==)
case Work.Sender(_, Some(ip), _, _) => analysisColl.count(BSONDocument(
))
case Work.Sender(_, Some(ip), _, _) => !analysisColl.exists($doc(
"sender.ip" -> ip
).some) map (0 ==)
))
case _ => fuccess(false)
}

View File

@ -4,6 +4,7 @@ import org.joda.time.DateTime
import chess.format.{ Uci, FEN }
import chess.variant.Variant
import lila.common.IpAddress
sealed trait Work {
def _id: Work.Id
@ -56,12 +57,14 @@ object Work {
case class Sender(
userId: Option[String],
ip: Option[String],
ip: Option[IpAddress],
mod: Boolean,
system: Boolean
) {
override def toString = if (system) "lichess" else userId orElse ip getOrElse "unknown"
override def toString =
if (system) "lichess"
else userId orElse ip.map(_.value) getOrElse "unknown"
}
case class Move(

View File

@ -214,14 +214,13 @@ object GameRepo {
def setHoldAlert(pov: Pov, mean: Int, sd: Int, ply: Option[Int] = None) = {
import Player.holdAlertBSONHandler
coll.update(
coll.updateField(
$id(pov.gameId),
$set(
s"p${pov.color.fold(0, 1)}.${Player.BSONFields.holdAlert}" ->
Player.HoldAlert(ply = ply | pov.game.turns, mean = mean, sd = sd)
)
s"p${pov.color.fold(0, 1)}.${Player.BSONFields.holdAlert}",
Player.HoldAlert(ply = ply | pov.game.turns, mean = mean, sd = sd)
).void
}
def setBorderAlert(pov: Pov) = setHoldAlert(pov, 0, 0, 20.some)
def finish(

View File

@ -20,7 +20,7 @@ private[lobby] final class SocketHandler(
blocking: String => Fu[Set[String]]
) {
private val HookPoolLimitPerMember = new lila.memo.RateLimit(
private val HookPoolLimitPerMember = new lila.memo.RateLimit[String](
credits = 25,
duration = 1 minute,
name = "lobby hook/pool per member",

View File

@ -8,7 +8,7 @@ import scala.concurrent.duration.Duration
/**
* side effect throttler that allows X ops per Y unit of time
*/
final class RateLimit(
final class RateLimit[K](
credits: Int,
duration: Duration,
name: String,
@ -20,7 +20,7 @@ final class RateLimit(
private val storage = Scaffeine()
.expireAfterWrite(duration)
.build[String, (Cost, ClearAt)]()
.build[K, (Cost, ClearAt)]()
private def makeClearAt = nowMillis + duration.toMillis
@ -29,7 +29,7 @@ final class RateLimit(
logger.info(s"[start] $name ($credits/$duration)")
def apply[A](k: String, cost: Cost = 1, msg: => String = "")(op: => A)(implicit default: Zero[A]): A =
def apply[A](k: K, cost: Cost = 1, msg: => String = "")(op: => A)(implicit default: Zero[A]): A =
storage getIfPresent k match {
case None =>
storage.put(k, cost -> makeClearAt)

View File

@ -1,5 +1,6 @@
package lila.mod
import lila.common.IpAddress
import lila.security.Permission
import lila.security.{ Firewall, UserSpy, Store => SecurityStore }
import lila.user.{ User, UserRepo, LightUserApi }
@ -79,8 +80,8 @@ final class ModApi(
UserRepo.toggleIpBan(user.id) zip
logApi.ban(mod, user.id, !user.ipBan) zip
user.ipBan.fold(
firewall unblockIps spy.ipStrings,
(spy.ipStrings map firewall.blockIp).sequenceFu >>
firewall unblockIps spy.ipStrings.map(IpAddress.apply),
(spy.ipStrings map { i => firewall blockIp IpAddress(i) }).sequenceFu >>
(SecurityStore disconnect user.id)
) void
}
@ -116,7 +117,7 @@ final class ModApi(
}
def ipban(mod: String, ip: String): Funit =
(firewall blockIp ip) >> logApi.ipban(mod, ip)
(firewall blockIp IpAddress(ip)) >> logApi.ipban(mod, ip)
private def withUser[A](username: String)(op: User => Fu[A]): Fu[A] =
UserRepo named username flatten "[mod] missing user " + username flatMap op

View File

@ -122,12 +122,15 @@ final class Env(
actor
}
lazy val selfReport = new SelfReport(roundMap)
lazy val socketHandler = new SocketHandler(
hub = hub,
roundMap = roundMap,
socketHub = socketHub,
messenger = messenger,
evalCacheHandler = evalCacheHandler,
selfReport = selfReport,
bus = system.lilaBus
)

View File

@ -0,0 +1,40 @@
package lila.round
import akka.actor._
import play.api.mvc.RequestHeader
import lila.common.{ IpAddress, HTTPRequest }
import lila.user.{ User, UserRepo }
final class SelfReport(roundMap: ActorRef) {
def apply(
userId: Option[User.ID],
ip: IpAddress,
fullId: String,
name: String
): Funit =
userId.??(UserRepo.named) flatMap { user =>
val known = user.??(_.engine)
lila.mon.cheat.cssBot()
// user.ifTrue(!known && name != "ceval") ?? { u =>
// Env.report.api.autoBotReport(u.id, referer, name)
// }
def doLog = lila.log("cheat").branch("jslog").info(
s"$ip https://lichess.org/$fullId ${user.fold("anon")(_.id)} $name"
)
if (fullId == "________") fuccess(doLog)
else lila.game.GameRepo pov fullId map {
_ ?? { pov =>
if (!known) doLog
if (Set("ceval", "rcb", "ccs")(name)) fuccess {
roundMap ! lila.hub.actorApi.map.Tell(
pov.gameId,
lila.round.actorApi.round.Cheat(pov.color)
)
}
else lila.game.GameRepo.setBorderAlert(pov)
}
}
}
}

View File

@ -9,8 +9,8 @@ import chess.format.Uci
import play.api.libs.json.{ JsObject, Json }
import actorApi._, round._
import lila.common.ApiVersion
import lila.common.PimpedJson._
import lila.common.{ IpAddress, ApiVersion }
import lila.game.{ Pov, PovRef, GameRepo }
import lila.hub.actorApi.map._
import lila.hub.actorApi.round.Berserk
@ -26,6 +26,7 @@ private[round] final class SocketHandler(
hub: lila.hub.Env,
messenger: Messenger,
evalCacheHandler: lila.evalCache.EvalCacheSocketHandler,
selfReport: SelfReport,
bus: lila.common.Bus
) {
@ -97,6 +98,10 @@ private[round] final class SocketHandler(
hub.actor.tournamentApi ! Berserk(gameId, userId)
member push ackEvent
}
case ("rep", o) => for {
d o obj "d"
name d str "n"
} selfReport(member.userId, member.ip, s"$gameId$playerId", name)
}: Handler.Controller) orElse lila.chat.Socket.in(
chatId = gameId,
member = member,
@ -111,7 +116,7 @@ private[round] final class SocketHandler(
colorName: String,
uid: Uid,
user: Option[User],
ip: String,
ip: IpAddress,
userTv: Option[String],
apiVersion: ApiVersion
): Fu[Option[JsSocketHandler]] =
@ -123,7 +128,7 @@ private[round] final class SocketHandler(
pov: Pov,
uid: Uid,
user: Option[User],
ip: String,
ip: IpAddress,
apiVersion: ApiVersion
): Fu[JsSocketHandler] =
join(pov, Some(pov.playerId), uid, user, ip, userTv = none, apiVersion)
@ -133,7 +138,7 @@ private[round] final class SocketHandler(
playerId: Option[String],
uid: Uid,
user: Option[User],
ip: String,
ip: IpAddress,
userTv: Option[String],
apiVersion: ApiVersion
): Fu[JsSocketHandler] = {

View File

@ -7,7 +7,7 @@ import scala.concurrent.Promise
import chess.Color
import chess.format.Uci
import lila.common.ApiVersion
import lila.common.{ IpAddress, ApiVersion }
import lila.game.Event
import lila.socket.SocketMember
import lila.socket.Socket.Uid
@ -20,7 +20,7 @@ sealed trait Member extends SocketMember {
val color: Color
val playerIdOption: Option[String]
val troll: Boolean
val ip: String
val ip: IpAddress
val userTv: Option[String]
val apiVersion: ApiVersion
@ -36,7 +36,7 @@ object Member {
user: Option[User],
color: Color,
playerIdOption: Option[String],
ip: String,
ip: IpAddress,
userTv: Option[String],
apiVersion: ApiVersion
): Member = {
@ -54,7 +54,7 @@ case class Owner(
playerId: String,
color: Color,
troll: Boolean,
ip: String,
ip: IpAddress,
apiVersion: ApiVersion
) extends Member {
@ -67,7 +67,7 @@ case class Watcher(
userId: Option[String],
color: Color,
troll: Boolean,
ip: String,
ip: IpAddress,
userTv: Option[String],
apiVersion: ApiVersion
) extends Member {
@ -80,7 +80,7 @@ case class Join(
user: Option[User],
color: Color,
playerId: Option[String],
ip: String,
ip: IpAddress,
userTv: Option[String],
apiVersion: ApiVersion
)
@ -138,7 +138,7 @@ package round {
case object Abandon
case class ForecastPlay(lastMove: chess.Move)
case class Cheat(color: Color)
case class HoldAlert(playerId: String, mean: Int, sd: Int, ip: String)
case class HoldAlert(playerId: String, mean: Int, sd: Int, ip: IpAddress)
case class GoBerserk(color: Color)
}

View File

@ -7,7 +7,7 @@ import play.api.data.Forms._
import play.api.mvc.RequestHeader
import reactivemongo.bson._
import lila.common.ApiVersion
import lila.common.{ ApiVersion, IpAddress }
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.user.{ User, UserRepo }
@ -92,7 +92,7 @@ final class Api(
def userIdsSharingFingerprint = userIdsSharingField("fp") _
def recentByIpExists(ip: String): Fu[Boolean] = Store recentByIpExists ip
def recentByIpExists(ip: IpAddress): Fu[Boolean] = Store recentByIpExists ip
private def userIdsSharingField(field: String)(userId: String): Fu[List[String]] =
coll.distinct[String, List](

View File

@ -8,7 +8,7 @@ import ornicar.scalalib.Random
import play.api.mvc.Results.Redirect
import play.api.mvc.{ RequestHeader, Action, Cookies }
import lila.common.LilaCookie
import lila.common.{ IpAddress, LilaCookie }
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
@ -34,14 +34,18 @@ final class Firewall(
def accepts(req: RequestHeader): Fu[Boolean] = blocks(req) map (!_)
def blockIp(ip: String): Funit = validIp(ip) ?? {
coll.update($id(ip), $doc("_id" -> ip, "date" -> DateTime.now), upsert = true).void >>- refresh
def blockIp(ip: IpAddress): Funit = validIp(ip) ?? {
coll.update(
$id(ip),
$doc("_id" -> ip, "date" -> DateTime.now),
upsert = true
).void >>- refresh
}
def unblockIps(ips: Iterable[String]): Funit =
coll.remove($inIds(ips filter validIp)).void >>- refresh
def unblockIps(ips: Iterable[IpAddress]): Funit =
coll.remove($inIds(ips.filter(validIp).map(_.value))).void >>- refresh
def blocksIp(ip: String): Fu[Boolean] = ips contains ip
def blocksIp(ip: IpAddress): Fu[Boolean] = ips contains ip.value
private def refresh {
ips.clear
@ -50,15 +54,9 @@ final class Firewall(
private def blocksCookies(cookies: Cookies, name: String) =
(cookies get name).isDefined
// http://stackoverflow.com/questions/106179/regular-expression-to-match-hostname-or-ip-address
private val ipv4Regex = """^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$""".r
// ipv6 address in standard form (no compression, no leading zeros)
private val ipv6Regex = """^((0|[1-9a-f][0-9a-f]{0,3}):){7}(0|[1-9a-f][0-9a-f]{0,3})""".r
private def validIp(ip: String) =
((ipv4Regex matches ip) && ip != "127.0.0.1" && ip != "0.0.0.0") ||
((ipv6Regex matches ip) && ip != "0:0:0:0:0:0:0:1" && ip != "0:0:0:0:0:0:0:0")
private def validIp(ip: IpAddress) =
(IpAddress.isv4(ip) && ip.value != "127.0.0.1" && ip.value != "0.0.0.0") ||
(IpAddress.isv6(ip) && ip.value != "0:0:0:0:0:0:0:1" && ip.value != "0:0:0:0:0:0:0:0")
private type IP = Vector[Byte]

View File

@ -4,7 +4,7 @@ import org.joda.time.DateTime
import play.api.mvc.RequestHeader
import reactivemongo.bson.Macros
import lila.common.{ HTTPRequest, ApiVersion }
import lila.common.{ HTTPRequest, ApiVersion, IpAddress }
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
@ -22,7 +22,7 @@ object Store {
coll.insert($doc(
"_id" -> sessionId,
"user" -> userId,
"ip" -> HTTPRequest.lastRemoteAddress(req),
"ip" -> HTTPRequest.lastRemoteAddress(req).value,
"ua" -> HTTPRequest.userAgent(req).|("?"),
"date" -> DateTime.now,
"up" -> true,
@ -132,6 +132,6 @@ object Store {
coll.remove($inIds(olds.map(_._id))).void
}
private[security] def recentByIpExists(ip: String): Fu[Boolean] =
private[security] def recentByIpExists(ip: IpAddress): Fu[Boolean] =
coll.exists($doc("ip" -> ip, "date" -> $gt(DateTime.now minusDays 7)))
}

View File

@ -1,5 +1,7 @@
package lila.security
import lila.common.IpAddress
import play.api.libs.ws.WS
import play.api.Play.current
@ -7,10 +9,10 @@ final class Tor(providerUrl: String) {
private var ips = Set[String]()
private[security] def refresh(withIps: Iterable[String] => Funit) {
private[security] def refresh(withIps: Iterable[IpAddress] => Funit) {
WS.url(providerUrl).get() map { res =>
ips = res.body.lines.filterNot(_ startsWith "#").toSet
withIps(ips)
withIps(ips map IpAddress.apply)
lila.mon.security.tor.node(ips.size)
}
}

View File

@ -1,5 +1,6 @@
package lila.security
import lila.common.IpAddress
import lila.db.dsl._
import lila.user.{ User, UserRepo }
@ -40,7 +41,9 @@ object UserSpy {
user UserRepo named userId flatten "[spy] user not found"
infos Store.findInfoByUser(user.id)
ips = infos.map(_.ip).distinct
blockedIps (ips map firewall.blocksIp).sequenceFu
blockedIps (ips map { i =>
firewall blocksIp IpAddress(i)
}).sequenceFu
locations <- scala.concurrent.Future {
ips map geoIP.orUnknown
}

View File

@ -20,7 +20,7 @@ object Handler {
val emptyController: Controller = PartialFunction.empty
private val AnaRateLimiter = new lila.memo.RateLimit(120, 30 seconds,
private val AnaRateLimiter = new lila.memo.RateLimit[String](120, 30 seconds,
name = "socket analysis move",
key = "socket_analysis_move")

View File

@ -30,7 +30,7 @@ private[study] final class SocketHandler(
import Handler.AnaRateLimit
import JsonView.shapeReader
private val InviteLimitPerUser = new lila.memo.RateLimit(
private val InviteLimitPerUser = new lila.memo.RateLimit[User.ID](
credits = 50,
duration = 24 hour,
name = "study invites per user",

View File

@ -22,7 +22,7 @@ sealed trait UserContext {
def troll = me.??(_.troll)
def ip = req.remoteAddress
def ip = lila.common.HTTPRequest lastRemoteAddress req
def kid = me.??(_.kid)
def noKid = !kid

View File

@ -98,4 +98,11 @@ module.exports = function(send, ctrl) {
}
return false;
}.bind(this);
lichess.pubsub.on('ab.rep', function(n) {
console.log(n);
send('rep', {
n: n
});
});
}