lila/modules/security/src/main/UserSpy.scala

231 lines
6.9 KiB
Scala

package lila.security
import org.joda.time.DateTime
import reactivemongo.api.ReadPreference
import reactivemongo.api.bson._
import org.uaparser.scala.{ Parser => UAParser }
import org.uaparser.scala.Client
import lila.common.{ EmailAddress, IpAddress }
import lila.db.dsl._
import lila.user.{ User, UserRepo }
case class UserSpy(
ips: List[UserSpy.IPData],
prints: List[UserSpy.FPData],
uas: List[Store.Dated[Client]],
otherUsers: List[UserSpy.OtherUser]
) {
import UserSpy.OtherUser
def rawIps = ips map (_.ip.value)
def rawFps = prints map (_.fp.value)
def otherUserIds = otherUsers.map(_.user.id)
def usersSharingIp =
otherUsers.collect {
case OtherUser(user, ips, _) if ips.nonEmpty => user
}
}
final class UserSpyApi(
firewall: Firewall,
store: Store,
userRepo: UserRepo,
geoIP: GeoIP,
ip2proxy: Ip2Proxy,
printBan: PrintBan
)(implicit ec: scala.concurrent.ExecutionContext) {
import UserSpy._
def apply(user: User, maxOthers: Int): Fu[UserSpy] =
store.chronoInfoByUser(user) flatMap { infos =>
val ips = distinctRecent(infos.map(_.datedIp))
val fps = distinctRecent(infos.flatMap(_.datedFp))
fetchOtherUsers(user, ips.map(_.value).toSet, fps.map(_.value).toSet, maxOthers) zip
ip2proxy.keepProxies(ips.map(_.value).toList) map {
case otherUsers ~ proxies =>
val othersByIp = otherUsers.foldLeft(Map.empty[IpAddress, Set[User]]) {
case (acc, other) =>
other.ips.foldLeft(acc) {
case (acc, ip) => acc.updated(ip, acc.getOrElse(ip, Set.empty) + other.user)
}
}
val othersByFp = otherUsers.foldLeft(Map.empty[FingerHash, Set[User]]) {
case (acc, other) =>
other.fps.foldLeft(acc) {
case (acc, fp) => acc.updated(fp, acc.getOrElse(fp, Set.empty) + other.user)
}
}
UserSpy(
ips = ips.map { ip =>
IPData(
ip,
firewall blocksIp ip.value,
geoIP orUnknown ip.value,
proxies(ip.value),
Alts(othersByIp.getOrElse(ip.value, Set.empty))
)
}.toList,
prints = fps.map { fp =>
FPData(
fp,
printBan blocks fp.value,
Alts(othersByFp.getOrElse(fp.value, Set.empty))
)
}.toList,
uas = distinctRecent(infos.map(_.datedUa map parseUa)).toList,
otherUsers = otherUsers
)
}
}
private[security] def userHasPrint(u: User): Fu[Boolean] =
store.coll.secondaryPreferred.exists($doc("user" -> u.id, "fp" $exists true))
private val parseUa = UAParser.default.parse _
private def fetchOtherUsers(
user: User,
ipSet: Set[IpAddress],
fpSet: Set[FingerHash],
max: Int
): Fu[List[OtherUser]] =
ipSet.nonEmpty ?? store.coll
.aggregateList(max, readPreference = ReadPreference.secondaryPreferred) { implicit framework =>
import framework._
import FingerHash.fpHandler
Match(
$doc(
$or(
"ip" $in ipSet,
"fp" $in fpSet
),
"user" $ne user.id,
"date" $gt DateTime.now.minusYears(1)
)
) -> List(
GroupField("user")(
"ips" -> AddFieldToSet("ip"),
"fps" -> AddFieldToSet("fp")
),
AddFields(
$doc(
"nbIps" -> $doc("$size" -> "$ips"),
"nbFps" -> $doc("$size" -> "$fps")
)
),
AddFields(
$doc(
"score" -> $doc(
"$add" -> $arr("$nbIps", "$nbFps", $doc("$multiply" -> $arr("$nbIps", "$nbFps")))
)
)
),
Sort(Descending("score")),
Limit(max),
PipelineOperator(
$doc(
"$lookup" -> $doc(
"from" -> userRepo.coll.name,
"localField" -> "_id",
"foreignField" -> "_id",
"as" -> "user"
)
)
),
UnwindField("user")
)
}
.map { docs =>
import lila.user.User.userBSONHandler
for {
doc <- docs
user <- doc.getAsOpt[User]("user")
ips <- doc.getAsOpt[Set[IpAddress]]("ips")
fps <- doc.getAsOpt[Set[FingerHash]]("fps")
} yield OtherUser(user, ips intersect ipSet, fps intersect fpSet)
}
def getUserIdsWithSameIpAndPrint(userId: User.ID): Fu[Set[User.ID]] =
for {
(ips, fps) <- nextValues("ip", userId, 100) zip nextValues("fp", userId, 100)
users <- (ips.nonEmpty && fps.nonEmpty) ?? store.coll.secondaryPreferred.distinctEasy[User.ID, Set](
"user",
$doc(
"ip" $in ips,
"fp" $in fps,
"user" $ne userId
)
)
} yield users
private def nextValues(field: String, userId: User.ID, max: Int): Fu[Set[String]] =
store.coll.secondaryPreferred.distinctEasy[String, Set](field, $doc("user" -> userId))
}
object UserSpy {
import Store.Dated
case class OtherUser(user: User, ips: Set[IpAddress], fps: Set[FingerHash]) {
val nbIps = ips.size
val nbFps = fps.size
val score = nbIps + nbFps + nbIps * nbFps
}
// distinct values, keeping the most recent of duplicated values
// assumes all is sorted by most recent first
def distinctRecent[V](all: List[Dated[V]]): scala.collection.View[Dated[V]] =
all
.foldLeft(Map.empty[V, DateTime]) {
case (acc, Dated(v, _)) if acc.contains(v) => acc
case (acc, Dated(v, date)) => acc + (v -> date)
}
.view
.map { case (v, date) => Dated(v, date) }
case class Alts(users: Set[User]) {
lazy val boosters = users.count(_.marks.boost)
lazy val engines = users.count(_.marks.engine)
lazy val trolls = users.count(_.marks.troll)
lazy val alts = users.count(_.marks.alt)
lazy val cleans = users.count(_.marks.clean)
def score = boosters + engines + trolls + alts - cleans
}
case class IPData(
ip: Dated[IpAddress],
blocked: Boolean,
location: Location,
proxy: Boolean,
alts: Alts
)
case class FPData(
fp: Dated[FingerHash],
banned: Boolean,
alts: Alts
)
case class WithMeSortedWithEmails(others: List[OtherUser], emails: Map[User.ID, EmailAddress]) {
def emailValueOf(u: User) = emails.get(u.id).map(_.value)
def size = others.size
}
def withMeSortedWithEmails(
userRepo: UserRepo,
me: User,
spy: UserSpy
)(implicit ec: scala.concurrent.ExecutionContext): Fu[WithMeSortedWithEmails] =
userRepo.emailMap(me.id :: spy.otherUsers.map(_.user.id)) map { emailMap =>
WithMeSortedWithEmails(
(OtherUser(me, spy.rawIps.toSet, spy.rawFps.toSet) :: spy.otherUsers),
emailMap
)
}
}