implement signup autokill

pull/3784/head
Thibault Duplessis 2017-11-10 11:28:17 -05:00
parent c530e08812
commit 6a36c36254
11 changed files with 128 additions and 51 deletions

View File

@ -7,7 +7,7 @@ import scala.concurrent.duration._
import lila.api.Context
import lila.app._
import lila.common.{ LilaCookie, HTTPRequest, IpAddress }
import lila.common.{ LilaCookie, HTTPRequest, IpAddress, EmailAddress }
import lila.memo.RateLimit
import lila.security.FingerPrint
import lila.user.{ UserRepo, User => UserModel }
@ -139,9 +139,7 @@ object Auth extends LilaController {
if (ipExists) fuccess(YesBecauseIp)
else print.??(api.recentByPrintExists) flatMap { printExists =>
if (printExists) fuccess(YesBecausePrint)
else Mod.ipIntelCache.get(ip).map(80 <)
.recover { case _: Exception => false }
.map { _.fold(YesBecauseProxy, Nope) }
else Env.security.ipIntel(ip).map(80 <).map { _.fold(YesBecauseProxy, Nope) }
}
}
}
@ -178,9 +176,9 @@ object Auth extends LilaController {
case (user, email) if mustConfirm.value =>
env.emailConfirm.send(user, email) >> {
if (env.emailConfirm.effective) Redirect(routes.Auth.checkYourEmail(user.username)).fuccess
else env.welcomeEmail(user, email) >> redirectNewUser(user)
else welcome(user, email) >> redirectNewUser(user)
}
case (user, email) => env.welcomeEmail(user, email) >> redirectNewUser(user)
case (user, email) => welcome(user, email) >> redirectNewUser(user)
}
}
}
@ -205,9 +203,9 @@ object Auth extends LilaController {
case (user, email) if mustConfirm.value =>
env.emailConfirm.send(user, email) >> {
if (env.emailConfirm.effective) Ok(Json.obj("email_confirm" -> true)).fuccess
else env.welcomeEmail(user, email) >> authenticateUser(user)
else welcome(user, email) >> authenticateUser(user)
}
case (user, _) => env.welcomeEmail(user, email) >> authenticateUser(user)
case (user, _) => welcome(user, email) >> authenticateUser(user)
}
}
}
@ -217,6 +215,10 @@ object Auth extends LilaController {
}
}
private def welcome(user: UserModel, email: EmailAddress)(implicit ctx: Context) =
Env.security.autoKill(user, HTTPRequest lastRemoteAddress ctx.req, email) >>
env.welcomeEmail(user, email)
def checkYourEmail(name: String) = Open { implicit ctx =>
OptionOk(UserRepo named name) { user =>
html.auth.checkYourEmail(user)
@ -232,7 +234,7 @@ object Auth extends LilaController {
authLog(user.username, s"Confirmed email")
lila.mon.user.register.confirmEmailResult(true)()
UserRepo.email(user.id).flatMap {
_.?? { env.welcomeEmail(user, _) }
_.?? { welcome(user, _) }
} >> redirectNewUser(user)
}
}

View File

@ -158,7 +158,7 @@ object Mod extends LilaController {
}
} zip
(Env.shutup.api getPublicLines user.id) zip
(Env.security userSpy user.id) zip
(Env.security userSpy user) zip
Env.user.noteApi.forMod(user.id) zip
Env.mod.logApi.userHistory(user.id) zip
Env.report.api.inquiries.ofModId(me.id) map {
@ -178,32 +178,8 @@ object Mod extends LilaController {
def communicationPublic(username: String) = communications(username, false)
def communicationPrivate(username: String) = communications(username, true)
private[controllers] val ipIntelCache =
Env.memo.asyncCache.multi[IpAddress, Int](
name = "ipIntel",
f = ip => {
import play.api.libs.ws.WS
import play.api.Play.current
val email = Env.api.Net.Email
val url = s"http://check.getipintel.net/check.php?ip=$ip&contact=$email"
WS.url(url).get().map(_.body).mon(_.security.proxy.request.time).flatMap { str =>
parseFloatOption(str).fold[Fu[Int]](fufail(s"Invalid ratio ${str.take(140)}")) { ratio =>
if (ratio < 0) fufail(s"Error code $ratio")
else fuccess((ratio * 100).toInt)
}
}.addEffects(
fail = _ => lila.mon.security.proxy.request.failure(),
succ = percent => {
lila.mon.security.proxy.percent(percent max 0)
lila.mon.security.proxy.request.success()
}
)
},
expireAfter = _.ExpireAfterAccess(3 days)
)
def ipIntel(ip: String) = Secure(_.IpBan) { ctx => me =>
ipIntelCache.get(IpAddress(ip)).map { Ok(_) }.recover {
Env.security.ipIntel.failable(IpAddress(ip)).map { Ok(_) }.recover {
case e: Exception => InternalServerError(e.getMessage)
}
}

View File

@ -241,7 +241,7 @@ object User extends LilaController {
def mod(username: String) = Secure(_.UserSpy) { implicit ctx => me =>
OptionFuOk(UserRepo named username) { user =>
UserRepo.emails(user.id) zip
(Env.security userSpy user.id) zip
(Env.security userSpy user) zip
Env.mod.assessApi.getPlayerAggregateAssessmentWithGames(user.id) zip
Env.mod.logApi.userHistory(user.id) zip
Env.plan.api.recentChargesOf(user) zip

View File

@ -257,7 +257,7 @@ lazy val irwin = module("irwin", Seq(common, db, user, game, tournament, mod)).s
libraryDependencies ++= provided(play.api, reactivemongo.driver)
)
lazy val security = module("security", Seq(common, hub, db, user, i18n)).settings(
lazy val security = module("security", Seq(common, hub, db, user, i18n, slack)).settings(
libraryDependencies ++= provided(play.api, reactivemongo.driver, maxmind, hasher)
)

View File

@ -4,6 +4,7 @@ import akka.actor._
import com.typesafe.config.Config
import lila.security.{ Firewall, UserSpy }
import lila.user.User
final class Env(
config: Config,
@ -14,7 +15,7 @@ final class Env(
firewall: Firewall,
reportApi: lila.report.ReportApi,
lightUserApi: lila.user.LightUserApi,
userSpy: String => Fu[UserSpy],
userSpy: User => Fu[UserSpy],
securityApi: lila.security.SecurityApi,
tournamentApi: lila.tournament.TournamentApi,
simulEnv: lila.simul.Env,

View File

@ -8,7 +8,7 @@ import lila.user.{ User, UserRepo, LightUserApi }
final class ModApi(
logApi: ModlogApi,
userSpy: User.ID => Fu[UserSpy],
userSpy: User => Fu[UserSpy],
firewall: Firewall,
reporter: akka.actor.ActorSelection,
reportApi: lila.report.ReportApi,
@ -79,7 +79,7 @@ final class ModApi(
}
def setBan(mod: Mod, prev: Suspect, value: Boolean): Funit = for {
spy <- userSpy(prev.user.id)
spy <- userSpy(prev.user)
sus = prev.set(_.copy(ipBan = value))
_ <- UserRepo.setIpBan(sus.user.id, sus.user.ipBan)
_ <- logApi.ban(mod, sus)

View File

@ -0,0 +1,39 @@
package lila.security
import org.joda.time.DateTime
import lila.common.{ EmailAddress, IpAddress }
import lila.user.{ User, UserRepo }
final class AutoKill(
userSpy: UserSpyApi,
ipIntel: IpIntel,
slack: lila.slack.SlackApi,
system: akka.actor.ActorSystem
) {
def apply(user: User, ip: IpAddress, email: EmailAddress): Funit = checkable(user, email) ?? {
userSpy(user) flatMap { spy =>
val others = spy.usersSharingFingerprint
.sortBy(-_.createdAt.getSeconds)
.takeWhile(_.createdAt.isAfter(DateTime.now minusDays 3))
.take(5)
(others.size > 2 && others.forall(killed)) ??
ipIntel(ip).map { 75 < _ }
}
} map {
_ ?? kill(user)
}
private def killed(user: User) =
user.troll && user.engine && !user.enabled
private def checkable(user: User, email: EmailAddress) =
email.value.endsWith("@yandex.ru")
private def kill(user: User): Funit = {
logger.info(s"Autokill ${user.username}")
slack.autoKill(user)
// just log for now.
}
}

View File

@ -7,13 +7,14 @@ final class Env(
config: Config,
captcher: akka.actor.ActorSelection,
authenticator: lila.user.Authenticator,
slack: lila.slack.SlackApi,
asyncCache: lila.memo.AsyncCache.Builder,
system: akka.actor.ActorSystem,
scheduler: lila.common.Scheduler,
db: lila.db.Env
) {
private val settings = new {
val NetBaseUrl = config getString "net.base_url"
val MailgunApiUrl = config getString "mailgun.api.url"
val MailgunApiKey = config getString "mailgun.api.key"
val MailgunSender = config getString "mailgun.sender"
@ -38,7 +39,9 @@ final class Env(
val RecaptchaPrivateKey = config getString "recaptcha.private_key"
val RecaptchaEndpoint = config getString "recaptcha.endpoint"
val RecaptchaEnabled = config getBoolean "recaptcha.enabled"
val NetBaseUrl = config getString "net.base_url"
val NetDomain = config getString "net.domain"
val NetEmail = config getString "net.email"
}
import settings._
@ -72,10 +75,15 @@ final class Env(
cacheTtl = GeoIPCacheTtl
)
lazy val userSpy = UserSpy(firewall, geoIP)(storeColl) _
lazy val userSpyApi = new UserSpyApi(firewall, geoIP, storeColl)
def userSpy = userSpyApi.apply _
def store = Store
lazy val ipIntel = new IpIntel(asyncCache, NetEmail)
lazy val autoKill = new AutoKill(userSpyApi, ipIntel, slack, system)
private lazy val mailgun = new Mailgun(
apiUrl = MailgunApiUrl,
apiKey = MailgunApiKey,
@ -143,6 +151,8 @@ object Env {
config = lila.common.PlayApp loadConfig "security",
db = lila.db.Env.current,
authenticator = lila.user.Env.current.authenticator,
slack = lila.slack.Env.current.api,
asyncCache = lila.memo.Env.current.asyncCache,
system = lila.common.PlayApp.system,
scheduler = lila.common.PlayApp.scheduler,
captcher = lila.hub.Env.current.actor.captcher

View File

@ -0,0 +1,38 @@
package lila.security
import play.api.libs.ws.WS
import play.api.Play.current
import scala.concurrent.duration._
import lila.common.IpAddress
final class IpIntel(asyncCache: lila.memo.AsyncCache.Builder, lichessEmail: String) {
def apply(ip: IpAddress): Fu[Int] = failable(ip) recover {
case e: Exception =>
logger.warn(s"IpIntel $ip", e)
0
}
def failable(ip: IpAddress): Fu[Int] = cache get ip
private val cache = asyncCache.multi[IpAddress, Int](
name = "ipIntel",
f = ip => {
val url = s"http://check.getipintel.net/check.php?ip=$ip&contact=$lichessEmail"
WS.url(url).get().map(_.body).mon(_.security.proxy.request.time).flatMap { str =>
parseFloatOption(str).fold[Fu[Int]](fufail(s"Invalid ratio ${str.take(140)}")) { ratio =>
if (ratio < 0) fufail(s"Error code $ratio")
else fuccess((ratio * 100).toInt)
}
}.addEffects(
fail = _ => lila.mon.security.proxy.request.failure(),
succ = percent => {
lila.mon.security.proxy.percent(percent max 0)
lila.mon.security.proxy.request.success()
}
)
},
expireAfter = _.ExpireAfterAccess(3 days)
)
}

View File

@ -31,17 +31,11 @@ case class UserSpy(
def otherUserIds = otherUsers.map(_.user.id)
}
object UserSpy {
private[security] final class UserSpyApi(firewall: Firewall, geoIP: GeoIP, coll: Coll) {
case class OtherUser(user: User, byIp: Boolean, byFingerprint: Boolean)
import UserSpy._
type Fingerprint = String
type Value = String
case class IPData(ip: IpAddress, blocked: Boolean, location: Location)
private[security] def apply(firewall: Firewall, geoIP: GeoIP)(coll: Coll)(userId: String): Fu[UserSpy] = for {
user UserRepo named userId flatten "[spy] user not found"
def apply(user: User): Fu[UserSpy] = for {
infos Store.findInfoByUser(user.id)
ips = infos.map(_.ip).distinct
blockedIps = ips map firewall.blocksIp
@ -84,3 +78,13 @@ object UserSpy {
}
}
}
object UserSpy {
case class OtherUser(user: User, byIp: Boolean, byFingerprint: Boolean)
type Fingerprint = String
type Value = String
case class IPData(ip: IpAddress, blocked: Boolean, location: Location)
}

View File

@ -81,6 +81,13 @@ final class SlackApi(
channel = "tavern"
))
def autoKill(u: User): Funit = client(SlackMessage(
username = u.username,
icon = "skull_and_crossbones",
text = s"AutoKill candidate: ${userLink(u.username)}",
channel = "tavern"
))
def publishError(msg: String): Funit = client(SlackMessage(
username = "lichess error",
icon = "lightning",