implement signup autokill
parent
c530e08812
commit
6a36c36254
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue