security module migration
parent
538527c649
commit
bd5fb84e6c
|
@ -199,14 +199,8 @@ geoip {
|
|||
security {
|
||||
collection.security = security
|
||||
collection.print_ban = print_ban
|
||||
collection.firewall = firewall
|
||||
flood.duration = 60 seconds
|
||||
firewall {
|
||||
cookie {
|
||||
enabled = false
|
||||
name=fEKHA4zI74ZrZrom
|
||||
}
|
||||
collection.firewall = firewall
|
||||
}
|
||||
geoip = ${geoip}
|
||||
password_reset {
|
||||
secret = "???"
|
||||
|
@ -254,7 +248,6 @@ oauth {
|
|||
access_token = oauth_access_token
|
||||
app = oauth_client
|
||||
}
|
||||
base_url = ${net.protocol}oauth.${net.domain}/
|
||||
}
|
||||
recaptcha {
|
||||
endpoint = "https://www.google.com/recaptcha/api/siteverify"
|
||||
|
|
|
@ -1,15 +1,32 @@
|
|||
package lila.common
|
||||
|
||||
import io.methvin.play.autoconfig._
|
||||
import play.api.ConfigLoader
|
||||
|
||||
object config {
|
||||
|
||||
implicit val maxPerPageLoader: ConfigLoader[MaxPerPage] =
|
||||
ConfigLoader(_.getInt).map(MaxPerPage.apply)
|
||||
case class CollName(value: String) extends AnyVal with StringValue
|
||||
|
||||
implicit val collNameLoader: ConfigLoader[CollName] =
|
||||
ConfigLoader(_.getString).map(CollName.apply)
|
||||
case class Secret(value: String) extends AnyVal {
|
||||
override def toString = "Secret(****)"
|
||||
}
|
||||
|
||||
implicit val SecretLoader: ConfigLoader[Secret] =
|
||||
ConfigLoader(_.getString).map(Secret.apply)
|
||||
case class BaseUrl(value: String) extends AnyVal with StringValue
|
||||
|
||||
case class NetConfig(
|
||||
domain: String,
|
||||
protocol: String,
|
||||
@ConfigName("base_url") baseUrl: BaseUrl,
|
||||
email: String
|
||||
)
|
||||
|
||||
implicit val maxPerPageLoader = intLoader(MaxPerPage.apply)
|
||||
implicit val collNameLoader = strLoader(CollName.apply)
|
||||
implicit val secretLoader = strLoader(Secret.apply)
|
||||
implicit val baseUrlLoader = strLoader(BaseUrl.apply)
|
||||
implicit val emailAddressLoader = strLoader(EmailAddress.apply)
|
||||
implicit val netLoader = AutoConfig.loader[NetConfig]
|
||||
|
||||
private def strLoader[A](f: String => A): ConfigLoader[A] = ConfigLoader(_.getString) map f
|
||||
private def intLoader[A](f: Int => A): ConfigLoader[A] = ConfigLoader(_.getInt) map f
|
||||
}
|
||||
|
|
|
@ -28,12 +28,6 @@ case class MaxPerSecond(value: Int) extends AnyVal with IntValue
|
|||
|
||||
case class IpAddress(value: String) extends AnyVal with StringValue
|
||||
|
||||
case class CollName(value: String) extends AnyVal with StringValue
|
||||
|
||||
case class Secret(value: String) extends AnyVal {
|
||||
override def toString = "Secret(****)"
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -10,7 +10,7 @@ import scala.concurrent.{ Await, ExecutionContext, Future }
|
|||
import scala.util.{ Failure, Success }
|
||||
|
||||
import dsl.Coll
|
||||
import lila.common.{ CollName, Chronometer }
|
||||
import lila.common.Chronometer
|
||||
import lila.common.config._
|
||||
|
||||
case class DbConfig(
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
package lila.event
|
||||
|
||||
import akka.actor._
|
||||
import com.typesafe.config.Config
|
||||
import play.api.Configuration
|
||||
|
||||
import lila.common.CollName
|
||||
import lila.common.config._
|
||||
|
||||
final class Env(
|
||||
config: Config,
|
||||
appConfig: Configuration,
|
||||
db: lila.db.Env,
|
||||
asyncCache: lila.memo.AsyncCache.Builder,
|
||||
system: ActorSystem
|
||||
asyncCache: lila.memo.AsyncCache.Builder
|
||||
) {
|
||||
|
||||
private val CollectionEvent = config getString "collection.event"
|
||||
|
||||
private lazy val eventColl = db(CollectionEvent)
|
||||
private lazy val eventColl = db(appConfig.get[CollName]("event.collection.event"))
|
||||
|
||||
lazy val api = new EventApi(coll = eventColl, asyncCache = asyncCache)
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
||||
lazy val current = "event" boot new Env(
|
||||
config = lila.common.PlayApp loadConfig "event",
|
||||
db = lila.db.Env.current,
|
||||
asyncCache = lila.memo.Env.current.asyncCache,
|
||||
system = lila.common.PlayApp.system
|
||||
)
|
||||
}
|
||||
|
|
|
@ -29,14 +29,14 @@ final class EventApi(
|
|||
expireAfter = _.ExpireAfterWrite(5 minutes)
|
||||
)
|
||||
|
||||
def fetchPromotable: Fu[List[Event]] = coll.find($doc(
|
||||
def fetchPromotable: Fu[List[Event]] = coll.ext.find($doc(
|
||||
"enabled" -> true,
|
||||
"startsAt" $gt DateTime.now.minusDays(1) $lt DateTime.now.plusDays(1)
|
||||
)).sort($doc("startsAt" -> 1)).list[Event](10).map {
|
||||
_.filter(_.featureNow) take 3
|
||||
}
|
||||
|
||||
def list = coll.find($empty).sort($doc("startsAt" -> -1)).list[Event](50)
|
||||
def list = coll.ext.find($empty).sort($doc("startsAt" -> -1)).list[Event](50)
|
||||
|
||||
def oneEnabled(id: String) = coll.byId[Event](id).map(_.filter(_.enabled))
|
||||
|
||||
|
@ -47,13 +47,13 @@ final class EventApi(
|
|||
}
|
||||
|
||||
def update(old: Event, data: EventForm.Data) =
|
||||
coll.update($id(old.id), data update old) >>- promotable.refresh
|
||||
coll.update.one($id(old.id), data update old) >>- promotable.refresh
|
||||
|
||||
def createForm = EventForm.form
|
||||
|
||||
def create(data: EventForm.Data, userId: String): Fu[Event] = {
|
||||
val event = data make userId
|
||||
coll.insert(event) >>- promotable.refresh inject event
|
||||
coll.insert.one(event) >>- promotable.refresh inject event
|
||||
}
|
||||
|
||||
def clone(old: Event) = old.copy(
|
||||
|
|
|
@ -3,8 +3,8 @@ package lila.memo
|
|||
import com.softwaremill.macwire._
|
||||
import io.methvin.play.autoconfig._
|
||||
import play.api.Configuration
|
||||
|
||||
import lila.db.dsl.Coll
|
||||
import lila.common.CollName
|
||||
import lila.common.config._
|
||||
|
||||
case class MemoConfig(
|
||||
|
|
|
@ -6,7 +6,6 @@ import io.methvin.play.autoconfig._
|
|||
import play.api.Configuration
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.CollName
|
||||
import lila.common.config._
|
||||
import lila.db.DbConfig
|
||||
import lila.db.dsl.Coll
|
||||
|
@ -14,7 +13,6 @@ import lila.db.Env.configLoader
|
|||
|
||||
private case class OauthConfig(
|
||||
mongodb: DbConfig,
|
||||
@ConfigName("base_url") baseUrl: String,
|
||||
@ConfigName("collection.access_token") tokenColl: CollName,
|
||||
@ConfigName("collection.app") appColl: CollName
|
||||
)
|
||||
|
@ -35,11 +33,20 @@ final class Env(
|
|||
|
||||
lazy val appApi = new OAuthAppApi(appColl)
|
||||
|
||||
// #TODO lila should be able to start without it
|
||||
lazy val server = {
|
||||
val mk = (coll: Coll) => wire[OAuthServer]
|
||||
mk(tokenColl)
|
||||
}
|
||||
|
||||
lazy val tryServer: OAuthServer.Try = () => scala.concurrent.Future {
|
||||
server.some
|
||||
}.withTimeoutDefault(50 millis, none)(system) recover {
|
||||
case e: Exception =>
|
||||
lila.log("security").warn("oauth", e)
|
||||
none
|
||||
}
|
||||
|
||||
lazy val tokenApi = new PersonalTokenApi(
|
||||
tokenColl = tokenColl
|
||||
)
|
||||
|
|
|
@ -52,13 +52,13 @@ final class OAuthServer(
|
|||
)
|
||||
|
||||
private def fetchAccessToken(tokenId: AccessToken.Id): Fu[Option[AccessToken.ForAuth]] =
|
||||
tokenColl.findAndUpdate(
|
||||
tokenColl.ext.findAndUpdate(
|
||||
selector = $doc(F.id -> tokenId),
|
||||
update = $set(F.usedAt -> DateTime.now),
|
||||
fields = AccessToken.forAuthProjection.some
|
||||
).map(_.value) map {
|
||||
_ ?? AccessToken.ForAuthBSONReader.readOpt
|
||||
}
|
||||
_ ?? AccessToken.ForAuthBSONReader.readOpt
|
||||
}
|
||||
}
|
||||
|
||||
object OAuthServer {
|
||||
|
|
|
@ -7,6 +7,7 @@ import lila.i18n.I18nKeys.{ emails => trans }
|
|||
import lila.user.{ User, UserRepo }
|
||||
|
||||
final class AutomaticEmail(
|
||||
userRepo: UserRepo,
|
||||
mailgun: Mailgun,
|
||||
baseUrl: String
|
||||
) {
|
||||
|
@ -31,8 +32,8 @@ ${Mailgun.txt.serviceNote}
|
|||
}
|
||||
|
||||
def onTitleSet(username: String)(implicit lang: Lang): Funit = for {
|
||||
user <- UserRepo named username flatten s"No such user $username"
|
||||
emailOption <- UserRepo email user.id
|
||||
user <- userRepo named username orFail s"No such user $username"
|
||||
emailOption <- userRepo email user.id
|
||||
} yield for {
|
||||
title <- user.title
|
||||
email <- emailOption
|
||||
|
@ -62,7 +63,7 @@ ${Mailgun.txt.serviceNote}
|
|||
}
|
||||
|
||||
def onBecomeCoach(user: User)(implicit lang: Lang): Funit =
|
||||
UserRepo email user.id flatMap {
|
||||
userRepo email user.id flatMap {
|
||||
_ ?? { email =>
|
||||
val body = s"""Hello,
|
||||
|
||||
|
@ -88,8 +89,8 @@ ${Mailgun.txt.serviceNote}
|
|||
}
|
||||
|
||||
def onFishnetKey(userId: User.ID, key: String)(implicit lang: Lang): Funit = for {
|
||||
user <- UserRepo named userId flatten s"No such user $userId"
|
||||
emailOption <- UserRepo email user.id
|
||||
user <- userRepo named userId orFail s"No such user $userId"
|
||||
emailOption <- userRepo email user.id
|
||||
} yield emailOption ?? { email =>
|
||||
|
||||
val body = s"""Hello,
|
||||
|
|
|
@ -3,9 +3,9 @@ package lila.security
|
|||
import scala.concurrent.duration._
|
||||
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.ws.{ WS, WSResponse }
|
||||
import play.api.Play.current
|
||||
import play.api.libs.ws.WSClient
|
||||
|
||||
import lila.common.config.Secret
|
||||
import lila.common.Domain
|
||||
import lila.db.dsl._
|
||||
|
||||
|
@ -13,13 +13,13 @@ import lila.db.dsl._
|
|||
* Only hit after trying everything else (DnsApi)
|
||||
* and save the result forever. */
|
||||
private final class CheckMail(
|
||||
url: String,
|
||||
key: String,
|
||||
ws: WSClient,
|
||||
config: SecurityConfig.CheckMail,
|
||||
mongoCache: lila.memo.MongoCache.Builder
|
||||
)(implicit system: akka.actor.ActorSystem) {
|
||||
|
||||
def apply(domain: Domain.Lower): Fu[Boolean] =
|
||||
if (key.isEmpty) fuccess(true)
|
||||
if (config.key.value.isEmpty) fuccess(true)
|
||||
else cache(domain).withTimeoutDefault(2.seconds, true) recover {
|
||||
case e: Exception =>
|
||||
lila.mon.security.checkMailApi.error()
|
||||
|
@ -27,12 +27,12 @@ private final class CheckMail(
|
|||
true
|
||||
}
|
||||
|
||||
private[security] def fetchAllBlocked: Fu[List[String]] = cache.coll.distinct[String, List](
|
||||
private[security] def fetchAllBlocked: Fu[List[String]] = cache.coll.distinctEasy[String, List](
|
||||
"_id",
|
||||
$doc(
|
||||
"_id" $regex s"^$prefix:",
|
||||
"v" -> false
|
||||
).some
|
||||
)
|
||||
) map { ids =>
|
||||
val dropSize = prefix.size + 1
|
||||
ids.map(_ drop dropSize)
|
||||
|
@ -48,9 +48,9 @@ private final class CheckMail(
|
|||
)
|
||||
|
||||
private def fetch(domain: Domain.Lower): Fu[Boolean] =
|
||||
WS.url(url)
|
||||
.withQueryString("domain" -> domain.value, "disable_test_connection" -> "true")
|
||||
.withHeaders("x-rapidapi-key" -> key)
|
||||
ws.url(config.url)
|
||||
.withQueryStringParameters("domain" -> domain.value, "disable_test_connection" -> "true")
|
||||
.withHttpHeaders("x-rapidapi-key" -> config.key.value)
|
||||
.get withTimeout 15.seconds map {
|
||||
case res if res.status == 200 =>
|
||||
val valid = ~(res.json \ "valid").asOpt[Boolean]
|
||||
|
@ -63,6 +63,6 @@ private final class CheckMail(
|
|||
if (!ok) lila.mon.security.checkMailApi.block()
|
||||
ok
|
||||
case res =>
|
||||
throw lila.base.LilaException(s"$url $domain ${res.status} ${res.body take 200}")
|
||||
throw lila.base.LilaException(s"${config.url} $domain ${res.status} ${res.body take 200}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,22 @@ package lila.security
|
|||
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
private[security] final class Cli extends lila.common.Cli {
|
||||
private[security] final class Cli(userRepo: UserRepo) extends lila.common.Cli {
|
||||
|
||||
def process = {
|
||||
|
||||
case "security" :: "roles" :: uid :: Nil =>
|
||||
UserRepo named uid map {
|
||||
userRepo named uid map {
|
||||
_.fold("User %s not found" format uid)(_.roles mkString " ")
|
||||
}
|
||||
|
||||
case "security" :: "grant" :: uid :: roles =>
|
||||
perform(uid, user =>
|
||||
UserRepo.setRoles(user.id, roles map (_.toUpperCase)).void)
|
||||
userRepo.setRoles(user.id, roles map (_.toUpperCase)).void)
|
||||
}
|
||||
|
||||
private def perform(username: String, op: User => Funit): Fu[String] =
|
||||
UserRepo named username flatMap { userOption =>
|
||||
userRepo named username flatMap { userOption =>
|
||||
userOption.fold(fufail[String]("User %s not found" format username)) { u =>
|
||||
op(u) inject "User %s successfully updated".format(username)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import lila.user.{ User, TotpSecret, UserRepo }
|
|||
import User.{ ClearPassword, TotpToken }
|
||||
|
||||
final class DataForm(
|
||||
userRepo: UserRepo,
|
||||
val captcher: akka.actor.ActorSelection,
|
||||
authenticator: lila.user.Authenticator,
|
||||
emailValidator: EmailAddressValidator
|
||||
|
@ -61,7 +62,7 @@ final class DataForm(
|
|||
error = "usernameCharsInvalid"
|
||||
)
|
||||
).verifying("usernameUnacceptable", u => !LameName.username(u))
|
||||
.verifying("usernameAlreadyUsed", u => !UserRepo.nameExists(u).awaitSeconds(4))
|
||||
.verifying("usernameAlreadyUsed", u => !userRepo.nameExists(u).awaitSeconds(4))
|
||||
|
||||
private val agreementBool = boolean.verifying(b => b)
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package lila.security
|
||||
|
||||
import play.api.libs.ws.WS
|
||||
import play.api.Play.current
|
||||
import play.api.libs.ws.WSClient
|
||||
|
||||
import lila.common.Domain
|
||||
|
||||
final class DisposableEmailDomain(
|
||||
ws: WSClient,
|
||||
providerUrl: String,
|
||||
checkMailBlocked: () => Fu[List[String]]
|
||||
) {
|
||||
|
@ -15,14 +15,14 @@ final class DisposableEmailDomain(
|
|||
private var regex = finalizeRegex(staticRegex)
|
||||
|
||||
private[security] def refresh: Unit = for {
|
||||
blacklist <- WS.url(providerUrl).get().map(_.body.lines) recover {
|
||||
blacklist <- ws.url(providerUrl).get().map(_.body.linesIterator) recover {
|
||||
case e: Exception =>
|
||||
logger.warn("DisposableEmailDomain.refresh", e)
|
||||
Iterator.empty
|
||||
}
|
||||
checked <- checkMailBlocked()
|
||||
} {
|
||||
val regexStr = s"${toRegexStr(blacklist)}|${toRegexStr(checked.toIterator)}"
|
||||
val regexStr = s"${toRegexStr(blacklist)}|${toRegexStr(checked.iterator)}"
|
||||
val nbDomains = regexStr.count('|' ==)
|
||||
lila.mon.email.disposableDomain(nbDomains)
|
||||
regex = finalizeRegex(s"$staticRegex|$regexStr")
|
||||
|
|
|
@ -2,16 +2,15 @@ package lila.security
|
|||
|
||||
import com.github.blemale.scaffeine.{ AsyncLoadingCache, Scaffeine }
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.ws.{ WS, WSResponse }
|
||||
import play.api.Play.current
|
||||
import play.api.libs.ws.WSClient
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.base.LilaException
|
||||
import lila.common.{ Chronometer, Domain }
|
||||
|
||||
private final class DnsApi(
|
||||
resolverUrl: String,
|
||||
fetchTimeout: FiniteDuration
|
||||
ws: WSClient,
|
||||
config: SecurityConfig.DnsApi
|
||||
)(implicit system: akka.actor.ActorSystem) {
|
||||
|
||||
// only valid email domains that are not whitelisted should make it here
|
||||
|
@ -38,10 +37,10 @@ private final class DnsApi(
|
|||
})
|
||||
|
||||
private def fetch[A](domain: Domain.Lower, tpe: String)(f: List[JsObject] => A): Fu[A] =
|
||||
WS.url(resolverUrl)
|
||||
.withQueryString("name" -> domain.value, "type" -> tpe)
|
||||
.withHeaders("Accept" -> "application/dns-json")
|
||||
.get withTimeout fetchTimeout map {
|
||||
ws.url(config.url)
|
||||
.withQueryStringParameters("name" -> domain.value, "type" -> tpe)
|
||||
.withHttpHeaders("Accept" -> "application/dns-json")
|
||||
.get withTimeout config.timeout map {
|
||||
case res if res.status == 200 || res.status == 404 => f(~(res.json \ "Answer").asOpt[List[JsObject]])
|
||||
case res => throw LilaException(s"Status ${res.status}")
|
||||
}
|
||||
|
|
|
@ -4,12 +4,13 @@ import play.api.data.validation._
|
|||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.{ EmailAddress, Domain }
|
||||
import lila.user.User
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
/**
|
||||
* Validate and normalize emails
|
||||
*/
|
||||
final class EmailAddressValidator(
|
||||
userRepo: UserRepo,
|
||||
disposable: DisposableEmailDomain,
|
||||
dnsApi: DnsApi,
|
||||
checkMail: CheckMail
|
||||
|
@ -29,14 +30,14 @@ final class EmailAddressValidator(
|
|||
* @return
|
||||
*/
|
||||
private def isTakenBySomeoneElse(email: EmailAddress, forUser: Option[User]): Fu[Boolean] =
|
||||
lila.user.UserRepo.idByEmail(email.normalize) map (_ -> forUser) map {
|
||||
userRepo.idByEmail(email.normalize) map (_ -> forUser) map {
|
||||
case (None, _) => false
|
||||
case (Some(userId), Some(user)) => userId != user.id
|
||||
case (_, _) => true
|
||||
}
|
||||
|
||||
private def wasUsedTwiceRecently(email: EmailAddress): Fu[Boolean] =
|
||||
lila.user.UserRepo.countRecentByPrevEmail(email.normalize).map(1<)
|
||||
userRepo.countRecentByPrevEmail(email.normalize).map(1<)
|
||||
|
||||
val acceptableConstraint = Constraint[String]("constraint.email_acceptable") { e =>
|
||||
if (isAcceptable(EmailAddress(e))) Valid
|
||||
|
|
|
@ -2,14 +2,16 @@ package lila.security
|
|||
|
||||
import scalatags.Text.all._
|
||||
|
||||
import lila.common.config._
|
||||
import lila.common.{ Lang, EmailAddress }
|
||||
import lila.i18n.I18nKeys.{ emails => trans }
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
final class EmailChange(
|
||||
userRepo: UserRepo,
|
||||
mailgun: Mailgun,
|
||||
baseUrl: String,
|
||||
tokenerSecret: String
|
||||
baseUrl: BaseUrl,
|
||||
tokenerSecret: Secret
|
||||
) {
|
||||
|
||||
import Mailgun.html._
|
||||
|
@ -45,7 +47,7 @@ ${Mailgun.txt.serviceNote}
|
|||
tokener read token map (_.flatten) flatMap {
|
||||
_ ?? {
|
||||
case TokenPayload(userId, email) =>
|
||||
UserRepo.setEmail(userId, email).nevermind >> UserRepo.byId(userId)
|
||||
userRepo.setEmail(userId, email).nevermind >> userRepo.byId(userId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +67,7 @@ ${Mailgun.txt.serviceNote}
|
|||
private val tokener = new StringToken[Option[TokenPayload]](
|
||||
secret = tokenerSecret,
|
||||
getCurrentValue = p => p ?? {
|
||||
case TokenPayload(userId, _) => UserRepo email userId map (_.??(_.value))
|
||||
case TokenPayload(userId, _) => userRepo email userId map (_.??(_.value))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ package lila.security
|
|||
|
||||
import scalatags.Text.all._
|
||||
|
||||
import lila.common.{ Lang, EmailAddress }
|
||||
import lila.common.config._
|
||||
import lila.common.{ Lang, EmailAddress, LilaCookie }
|
||||
import lila.i18n.I18nKeys.{ emails => trans }
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
|
@ -15,19 +16,20 @@ trait EmailConfirm {
|
|||
def confirm(token: String): Fu[EmailConfirm.Result]
|
||||
}
|
||||
|
||||
object EmailConfirmSkip extends EmailConfirm {
|
||||
final class EmailConfirmSkip(userRepo: UserRepo) extends EmailConfirm {
|
||||
|
||||
def effective = false
|
||||
|
||||
def send(user: User, email: EmailAddress)(implicit lang: Lang) = UserRepo setEmailConfirmed user.id void
|
||||
def send(user: User, email: EmailAddress)(implicit lang: Lang) = userRepo setEmailConfirmed user.id void
|
||||
|
||||
def confirm(token: String): Fu[EmailConfirm.Result] = fuccess(EmailConfirm.Result.NotFound)
|
||||
}
|
||||
|
||||
final class EmailConfirmMailgun(
|
||||
userRepo: UserRepo,
|
||||
mailgun: Mailgun,
|
||||
baseUrl: String,
|
||||
tokenerSecret: String
|
||||
baseUrl: BaseUrl,
|
||||
tokenerSecret: Secret
|
||||
) extends EmailConfirm {
|
||||
|
||||
import Mailgun.html._
|
||||
|
@ -70,11 +72,11 @@ ${trans.emailConfirm_ignore.literalTxtTo(lang, List("https://lichess.org"))}
|
|||
import EmailConfirm.Result
|
||||
|
||||
def confirm(token: String): Fu[Result] = tokener read token flatMap {
|
||||
_ ?? UserRepo.enabledById
|
||||
_ ?? userRepo.enabledById
|
||||
} flatMap {
|
||||
_.fold[Fu[Result]](fuccess(Result.NotFound)) { user =>
|
||||
UserRepo.mustConfirmEmail(user.id) flatMap {
|
||||
case true => (UserRepo setEmailConfirmed user.id) inject Result.JustConfirmed(user)
|
||||
userRepo.mustConfirmEmail(user.id) flatMap {
|
||||
case true => (userRepo setEmailConfirmed user.id) inject Result.JustConfirmed(user)
|
||||
case false => fuccess(Result.AlreadyConfirmed(user))
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +84,7 @@ ${trans.emailConfirm_ignore.literalTxtTo(lang, List("https://lichess.org"))}
|
|||
|
||||
private val tokener = new StringToken[User.ID](
|
||||
secret = tokenerSecret,
|
||||
getCurrentValue = id => UserRepo email id map (_.??(_.value))
|
||||
getCurrentValue = id => userRepo email id map (_.??(_.value))
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -97,7 +99,7 @@ object EmailConfirm {
|
|||
|
||||
case class UserEmail(username: String, email: EmailAddress)
|
||||
|
||||
object cookie {
|
||||
final class CookieApi(lilaCookie: LilaCookie) {
|
||||
|
||||
import play.api.mvc.{ Cookie, RequestHeader }
|
||||
|
||||
|
@ -108,10 +110,11 @@ object EmailConfirm {
|
|||
case Array(username, email) => UserEmail(username, EmailAddress(email))
|
||||
}
|
||||
|
||||
def make(user: User, email: EmailAddress)(implicit req: RequestHeader): Cookie = lila.common.LilaCookie.session(
|
||||
name = name,
|
||||
value = s"${user.username}$sep${email.value}"
|
||||
)
|
||||
def make(user: User, email: EmailAddress)(implicit req: RequestHeader): Cookie =
|
||||
lilaCookie.session(
|
||||
name = name,
|
||||
value = s"${user.username}$sep${email.value}"
|
||||
)
|
||||
}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
@ -171,11 +174,11 @@ object EmailConfirm {
|
|||
))
|
||||
)
|
||||
|
||||
def getStatus(username: String): Fu[Status] = UserRepo withEmails username flatMap {
|
||||
def getStatus(userRepo: UserRepo, username: String): Fu[Status] = userRepo withEmails username flatMap {
|
||||
case None => fuccess(NoSuchUser(username))
|
||||
case Some(User.WithEmails(user, emails)) =>
|
||||
if (!user.enabled) fuccess(Closed(username))
|
||||
else UserRepo mustConfirmEmail user.id map {
|
||||
else userRepo mustConfirmEmail user.id map {
|
||||
case true => emails.current match {
|
||||
case None => NoEmail(user.username)
|
||||
case Some(email) => EmailSent(user.username, email)
|
||||
|
|
|
@ -1,99 +1,63 @@
|
|||
package lila.security
|
||||
|
||||
import lila.common.{ EmailAddress, Strings, Iso }
|
||||
import akka.actor._
|
||||
import com.softwaremill.macwire._
|
||||
import io.methvin.play.autoconfig._
|
||||
import play.api.Configuration
|
||||
import play.api.libs.ws.WSClient
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.config._
|
||||
import lila.common.{ Bus, Strings, Iso, EmailAddress }
|
||||
import lila.memo.SettingStore.Formable.stringsFormable
|
||||
import lila.memo.SettingStore.Strings._
|
||||
import lila.oauth.OAuthServer
|
||||
|
||||
import akka.actor._
|
||||
import com.typesafe.config.Config
|
||||
import scala.concurrent.duration._
|
||||
import lila.common.Bus
|
||||
import lila.user.{ UserRepo, Authenticator }
|
||||
|
||||
final class Env(
|
||||
config: Config,
|
||||
appConfig: Configuration,
|
||||
ws: WSClient,
|
||||
captcher: ActorSelection,
|
||||
authenticator: lila.user.Authenticator,
|
||||
userRepo: UserRepo,
|
||||
authenticator: Authenticator,
|
||||
slack: lila.slack.SlackApi,
|
||||
asyncCache: lila.memo.AsyncCache.Builder,
|
||||
settingStore: lila.memo.SettingStore.Builder,
|
||||
tryOAuthServer: OAuthServer.Try,
|
||||
mongoCache: lila.memo.MongoCache.Builder,
|
||||
system: ActorSystem,
|
||||
scheduler: lila.common.Scheduler,
|
||||
scheduler: Scheduler,
|
||||
db: lila.db.Env,
|
||||
lifecycle: play.api.inject.ApplicationLifecycle
|
||||
) {
|
||||
|
||||
private val MailgunApiUrl = config getString "mailgun.api.url"
|
||||
private val MailgunApiKey = config getString "mailgun.api.key"
|
||||
private val MailgunSender = config getString "mailgun.sender"
|
||||
private val MailgunReplyTo = config getString "mailgun.reply_to"
|
||||
private val CollectionSecurity = config getString "collection.security"
|
||||
private val CollectionPrintBan = config getString "collection.print_ban"
|
||||
private val FirewallCookieName = config getString "firewall.cookie.name"
|
||||
private val FirewallCookieEnabled = config getBoolean "firewall.cookie.enabled"
|
||||
private val FirewallCollectionFirewall = config getString "firewall.collection.firewall"
|
||||
private val FloodDuration = config duration "flood.duration"
|
||||
private val GeoIPFile = config getString "geoip.file"
|
||||
private val GeoIPCacheTtl = config duration "geoip.cache_ttl"
|
||||
private val EmailConfirmSecret = config getString "email_confirm.secret"
|
||||
private val EmailConfirmEnabled = config getBoolean "email_confirm.enabled"
|
||||
private val PasswordResetSecret = config getString "password_reset.secret"
|
||||
private val EmailChangeSecret = config getString "email_change.secret"
|
||||
private val LoginTokenSecret = config getString "login_token.secret"
|
||||
private val TorProviderUrl = config getString "tor.provider_url"
|
||||
private val TorRefreshDelay = config duration "tor.refresh_delay"
|
||||
private val DisposableEmailProviderUrl = config getString "disposable_email.provider_url"
|
||||
private val DisposableEmailRefreshDelay = config duration "disposable_email.refresh_delay"
|
||||
private val RecaptchaPrivateKey = config getString "recaptcha.private_key"
|
||||
private val RecaptchaEndpoint = config getString "recaptcha.endpoint"
|
||||
private val NetBaseUrl = config getString "net.base_url"
|
||||
private val NetDomain = config getString "net.domain"
|
||||
private val IpIntelEmail = EmailAddress(config getString "ipintel.email")
|
||||
private val DnsApiUrl = config getString "dns_api.url"
|
||||
private val DnsApiTimeout = config duration "dns_api.timeout"
|
||||
private val CheckMailUrl = config getString "check_mail_api.url"
|
||||
private val CheckMailKey = config getString "check_mail_api.key"
|
||||
private val config = appConfig.get[SecurityConfig.Root]("security")(SecurityConfig.loader)
|
||||
|
||||
val recaptchaPublicConfig = RecaptchaPublicConfig(
|
||||
key = config getString "recaptcha.public_key",
|
||||
enabled = config getBoolean "recaptcha.enabled"
|
||||
)
|
||||
val recaptchaPublicConfig = config.recaptcha.public
|
||||
|
||||
lazy val firewall = new Firewall(
|
||||
coll = firewallColl,
|
||||
cookieName = FirewallCookieName.some filter (_ => FirewallCookieEnabled),
|
||||
system = system
|
||||
coll = db(config.collection.firewall),
|
||||
scheduler = scheduler
|
||||
)
|
||||
|
||||
lazy val flood = new Flood(FloodDuration)
|
||||
lazy val flood = new Flood(config.floodDuration)
|
||||
|
||||
lazy val recaptcha: Recaptcha =
|
||||
if (recaptchaPublicConfig.enabled) new RecaptchaGoogle(
|
||||
privateKey = RecaptchaPrivateKey,
|
||||
endpoint = RecaptchaEndpoint,
|
||||
lichessHostname = NetDomain
|
||||
)
|
||||
if (recaptchaPublicConfig.enabled) wire[RecaptchaGoogle]
|
||||
else RecaptchaSkip
|
||||
|
||||
lazy val forms = new DataForm(
|
||||
captcher = captcher,
|
||||
authenticator = authenticator,
|
||||
emailValidator = emailAddressValidator
|
||||
)
|
||||
lazy val forms = wire[DataForm]
|
||||
|
||||
lazy val geoIP = new GeoIP(
|
||||
file = GeoIPFile,
|
||||
cacheTtl = GeoIPCacheTtl
|
||||
)
|
||||
lazy val geoIP = wire[GeoIP]
|
||||
|
||||
lazy val userSpyApi = new UserSpyApi(firewall, geoIP, storeColl)
|
||||
def userSpy = userSpyApi.apply _
|
||||
lazy val userSpyApi = wire[UserSpyApi]
|
||||
|
||||
def store = Store
|
||||
lazy val store = new Store(db(config.collection.security))
|
||||
|
||||
lazy val ipIntel = new IpIntel(asyncCache, IpIntelEmail)
|
||||
lazy val ipIntel = {
|
||||
def mk = (email: EmailAddress) => wire[IpIntel]
|
||||
mk(config.ipIntelEmail)
|
||||
}
|
||||
|
||||
lazy val ugcArmedSetting = settingStore[Boolean](
|
||||
"ugcArmed",
|
||||
|
@ -101,24 +65,14 @@ final class Env(
|
|||
text = "Enable the user garbage collector".some
|
||||
)
|
||||
|
||||
lazy val printBan = new PrintBan(printBanColl)
|
||||
lazy val printBan = new PrintBan(db(config.collection.printBan))
|
||||
|
||||
lazy val garbageCollector = new GarbageCollector(
|
||||
userSpyApi,
|
||||
ipTrust,
|
||||
printBan,
|
||||
slack,
|
||||
ugcArmedSetting.get,
|
||||
system
|
||||
)
|
||||
lazy val garbageCollector = {
|
||||
def mk = (isArmed: () => Boolean) => wire[GarbageCollector]
|
||||
mk(ugcArmedSetting.get)
|
||||
}
|
||||
|
||||
private lazy val mailgun = new Mailgun(
|
||||
apiUrl = MailgunApiUrl,
|
||||
apiKey = MailgunApiKey,
|
||||
from = MailgunSender,
|
||||
replyTo = MailgunReplyTo,
|
||||
system = system
|
||||
)
|
||||
private lazy val mailgun = wire[Mailgun]
|
||||
|
||||
lazy val emailConfirm: EmailConfirm =
|
||||
if (EmailConfirmEnabled) new EmailConfirmMailgun(
|
||||
|
@ -128,41 +82,34 @@ final class Env(
|
|||
)
|
||||
else EmailConfirmSkip
|
||||
|
||||
lazy val passwordReset = new PasswordReset(
|
||||
mailgun = mailgun,
|
||||
baseUrl = NetBaseUrl,
|
||||
tokenerSecret = PasswordResetSecret
|
||||
)
|
||||
lazy val passwordReset = {
|
||||
def mk = (u: BaseUrl, s: Secret) => wire[PasswordReset]
|
||||
mk(config.net.baseUrl, config.passwordResetSecret)
|
||||
}
|
||||
|
||||
lazy val magicLink = new MagicLink(
|
||||
mailgun = mailgun,
|
||||
baseUrl = NetBaseUrl,
|
||||
tokenerSecret = PasswordResetSecret
|
||||
)
|
||||
lazy val magicLink = {
|
||||
def mk = (u: BaseUrl, s: Secret) => wire[MagicLink]
|
||||
mk(config.net.baseUrl, config.passwordResetSecret)
|
||||
}
|
||||
|
||||
lazy val emailChange = new EmailChange(
|
||||
mailgun = mailgun,
|
||||
baseUrl = NetBaseUrl,
|
||||
tokenerSecret = EmailChangeSecret
|
||||
)
|
||||
lazy val emailChange = {
|
||||
def mk = (u: BaseUrl, s: Secret) => wire[EmailChange]
|
||||
mk(config.net.baseUrl, config.emailChangeSecret)
|
||||
}
|
||||
|
||||
lazy val loginToken = new LoginToken(
|
||||
secret = LoginTokenSecret
|
||||
)
|
||||
lazy val loginToken = new LoginToken(config.loginTokenSecret, userRepo)
|
||||
|
||||
lazy val automaticEmail = new AutomaticEmail(
|
||||
mailgun = mailgun,
|
||||
baseUrl = NetBaseUrl
|
||||
)
|
||||
lazy val automaticEmail = wire[AutomaticEmail]
|
||||
|
||||
private lazy val dnsApi = new DnsApi(DnsApiUrl, DnsApiTimeout)(system)
|
||||
private lazy val dnsApi = wire[DnsApi]
|
||||
|
||||
private lazy val checkMail = new CheckMail(CheckMailUrl, CheckMailKey, mongoCache)(system)
|
||||
private lazy val checkMail: CheckMail = wire[CheckMail]
|
||||
|
||||
lazy val emailAddressValidator = new EmailAddressValidator(disposableEmailDomain, dnsApi, checkMail)
|
||||
lazy val emailAddressValidator = wire[EmailAddressValidator]
|
||||
|
||||
private lazy val disposableEmailDomain = new DisposableEmailDomain(
|
||||
providerUrl = DisposableEmailProviderUrl,
|
||||
ws = ws,
|
||||
providerUrl = config.disposableEmail.providerUrl,
|
||||
checkMailBlocked = () => checkMail.fetchAllBlocked
|
||||
)
|
||||
|
||||
|
@ -178,7 +125,7 @@ final class Env(
|
|||
lazy val spam = new Spam(spamKeywordsSetting.get)
|
||||
|
||||
scheduler.once(30 seconds)(disposableEmailDomain.refresh)
|
||||
scheduler.effect(DisposableEmailRefreshDelay, "Refresh disposable email domains")(disposableEmailDomain.refresh)
|
||||
scheduler.effect(config.disposableEmail.refreshDelay, "Refresh disposable email domains")(disposableEmailDomain.refresh)
|
||||
|
||||
lazy val tor = new Tor(TorProviderUrl)
|
||||
scheduler.once(31 seconds)(tor.refresh(_ => funit))
|
||||
|
@ -196,34 +143,4 @@ final class Env(
|
|||
case lila.hub.actorApi.fishnet.NewKey(userId, key) =>
|
||||
automaticEmail.onFishnetKey(userId, key)(lila.i18n.defaultLang)
|
||||
}
|
||||
|
||||
private[security] lazy val storeColl = db(CollectionSecurity)
|
||||
private[security] lazy val printBanColl = db(CollectionPrintBan)
|
||||
private[security] lazy val firewallColl = db(FirewallCollectionFirewall)
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
||||
private lazy val system = lila.common.PlayApp.system
|
||||
|
||||
lazy val current = "security" boot new 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,
|
||||
settingStore = lila.memo.Env.current.settingStore,
|
||||
tryOAuthServer = () => scala.concurrent.Future {
|
||||
lila.oauth.Env.current.server.some
|
||||
}.withTimeoutDefault(50 millis, none)(system) recover {
|
||||
case e: Exception =>
|
||||
lila.log("security").warn("oauth", e)
|
||||
none
|
||||
},
|
||||
mongoCache = lila.memo.Env.current.mongoCache,
|
||||
system = system,
|
||||
scheduler = lila.common.PlayApp.scheduler,
|
||||
captcher = lila.hub.Env.current.captcher,
|
||||
lifecycle = lila.common.PlayApp.lifecycle
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package lila.security
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import play.api.mvc.{ RequestHeader, Cookies }
|
||||
import play.api.mvc.RequestHeader
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.Future
|
||||
|
||||
import lila.common.IpAddress
|
||||
import lila.db.BSON.BSONJodaDateTimeHandler
|
||||
|
@ -11,48 +11,46 @@ import lila.db.dsl._
|
|||
|
||||
final class Firewall(
|
||||
coll: Coll,
|
||||
cookieName: Option[String],
|
||||
system: akka.actor.ActorSystem
|
||||
scheduler: akka.actor.Scheduler
|
||||
) {
|
||||
|
||||
private var current: Set[String] = Set.empty
|
||||
|
||||
system.scheduler.scheduleOnce(10 minutes)(loadFromDb)
|
||||
scheduler.scheduleOnce(10 minutes)(loadFromDb)
|
||||
|
||||
def blocksIp(ip: IpAddress): Boolean = current contains ip.value
|
||||
|
||||
def blocks(req: RequestHeader): Boolean = {
|
||||
val v = blocksIp {
|
||||
lila.common.HTTPRequest lastRemoteAddress req
|
||||
} || cookieName.?? { blocksCookies(req.cookies, _) }
|
||||
}
|
||||
if (v) lila.mon.security.firewall.block()
|
||||
v
|
||||
}
|
||||
|
||||
def accepts(req: RequestHeader): Boolean = !blocks(req)
|
||||
|
||||
def blockIps(ips: List[IpAddress]): Funit = ips.map { ip =>
|
||||
validIp(ip) ?? {
|
||||
coll.update(
|
||||
$id(ip),
|
||||
$doc("_id" -> ip, "date" -> DateTime.now),
|
||||
upsert = true
|
||||
).void
|
||||
def blockIps(ips: List[IpAddress]): Funit = Future.sequence {
|
||||
ips.map { ip =>
|
||||
validIp(ip) ?? {
|
||||
coll.update(
|
||||
$id(ip),
|
||||
$doc("_id" -> ip, "date" -> DateTime.now),
|
||||
upsert = true
|
||||
).void
|
||||
}
|
||||
}
|
||||
}.sequenceFu >> loadFromDb
|
||||
} >> loadFromDb
|
||||
|
||||
def unblockIps(ips: Iterable[IpAddress]): Funit =
|
||||
coll.remove($inIds(ips.filter(validIp))).void >>- loadFromDb
|
||||
|
||||
private def loadFromDb: Funit =
|
||||
coll.distinct[String, Set]("_id", none).map { ips =>
|
||||
coll.distinctEasy[String, Set]("_id", $empty).map { ips =>
|
||||
current = ips
|
||||
lila.mon.security.firewall.ip(ips.size)
|
||||
}
|
||||
|
||||
private def blocksCookies(cookies: Cookies, name: String) =
|
||||
(cookies get name).isDefined
|
||||
|
||||
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")
|
||||
|
|
|
@ -2,16 +2,17 @@ package lila.security
|
|||
|
||||
import com.github.blemale.scaffeine.{ LoadingCache, Scaffeine }
|
||||
import com.sanoma.cda.geoip.{ MaxMindIpGeo, IpLocation }
|
||||
import io.methvin.play.autoconfig._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.IpAddress
|
||||
|
||||
final class GeoIP(file: String, cacheTtl: FiniteDuration) {
|
||||
final class GeoIP(config: GeoIP.Config) {
|
||||
|
||||
private val geoIp = MaxMindIpGeo(file, 0)
|
||||
private val geoIp = MaxMindIpGeo(config.file, 0)
|
||||
|
||||
private val cache: LoadingCache[IpAddress, Option[Location]] = Scaffeine()
|
||||
.expireAfterAccess(cacheTtl)
|
||||
.expireAfterAccess(config.cacheTtl)
|
||||
.build(compute)
|
||||
|
||||
private def compute(ip: IpAddress): Option[Location] =
|
||||
|
@ -22,6 +23,14 @@ final class GeoIP(file: String, cacheTtl: FiniteDuration) {
|
|||
def orUnknown(ip: IpAddress): Location = apply(ip) | Location.unknown
|
||||
}
|
||||
|
||||
object GeoIP {
|
||||
case class Config(
|
||||
file: String,
|
||||
@ConfigName("cache_ttl") cacheTtl: FiniteDuration
|
||||
)
|
||||
implicit val configLoader = AutoConfig.loader[Config]
|
||||
}
|
||||
|
||||
case class Location(
|
||||
country: String,
|
||||
region: Option[String],
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package lila.security
|
||||
|
||||
import play.api.libs.ws.WS
|
||||
import play.api.Play.current
|
||||
import play.api.libs.ws.WSClient
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.{ IpAddress, EmailAddress }
|
||||
|
||||
final class IpIntel(asyncCache: lila.memo.AsyncCache.Builder, contactEmail: EmailAddress) {
|
||||
final class IpIntel(
|
||||
ws: WSClient,
|
||||
asyncCache: lila.memo.AsyncCache.Builder,
|
||||
contactEmail: EmailAddress
|
||||
) {
|
||||
|
||||
def apply(ip: IpAddress): Fu[Int] = failable(ip) recover {
|
||||
case e: Exception =>
|
||||
|
@ -22,7 +25,7 @@ final class IpIntel(asyncCache: lila.memo.AsyncCache.Builder, contactEmail: Emai
|
|||
name = "ipIntel",
|
||||
f = ip => {
|
||||
val url = s"https://check.getipintel.net/check.php?ip=$ip&contact=${contactEmail.value}"
|
||||
WS.url(url).get().map(_.body).mon(_.security.proxy.request.time).flatMap { str =>
|
||||
ws.url(url).get().map(_.body).mon(_.security.proxy.request.time).flatMap { str =>
|
||||
str.toFloatOption.fold[Fu[Int]](fufail(s"Invalid ratio ${str.take(140)}")) { ratio =>
|
||||
if (ratio < 0) fufail(s"IpIntel error $ratio on $url")
|
||||
else fuccess((ratio * 100).toInt)
|
||||
|
|
|
@ -4,21 +4,22 @@ import org.joda.time.DateTime
|
|||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.Iso
|
||||
import lila.common.config.Secret
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
final class LoginToken(secret: String) {
|
||||
final class LoginToken(secret: Secret, userRepo: UserRepo) {
|
||||
|
||||
def generate(user: User): Fu[String] = tokener make user.id
|
||||
|
||||
def consume(token: String): Fu[Option[User]] =
|
||||
tokener read token flatMap { _ ?? UserRepo.byId }
|
||||
tokener read token flatMap { _ ?? userRepo.byId }
|
||||
|
||||
private val tokener = LoginToken.makeTokener(secret, 1 minute)
|
||||
}
|
||||
|
||||
private object LoginToken {
|
||||
|
||||
def makeTokener(secret: String, lifetime: FiniteDuration) = new StringToken[User.ID](
|
||||
def makeTokener(secret: Secret, lifetime: FiniteDuration) = new StringToken[User.ID](
|
||||
secret = secret,
|
||||
getCurrentValue = _ => fuccess(DateStr toStr DateTime.now),
|
||||
currentValueHashSize = none,
|
||||
|
|
|
@ -4,13 +4,15 @@ import scala.concurrent.duration._
|
|||
import scalatags.Text.all._
|
||||
|
||||
import lila.common.{ Lang, EmailAddress }
|
||||
import lila.common.config._
|
||||
import lila.i18n.I18nKeys.{ emails => trans }
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
final class MagicLink(
|
||||
mailgun: Mailgun,
|
||||
baseUrl: String,
|
||||
tokenerSecret: String
|
||||
userRepo: UserRepo,
|
||||
baseUrl: BaseUrl,
|
||||
tokenerSecret: Secret
|
||||
) {
|
||||
|
||||
import Mailgun.html._
|
||||
|
@ -40,7 +42,7 @@ ${Mailgun.txt.serviceNote}
|
|||
}
|
||||
|
||||
def confirm(token: String): Fu[Option[User]] =
|
||||
tokener read token flatMap { _ ?? UserRepo.byId }
|
||||
tokener read token flatMap { _ ?? userRepo.byId }
|
||||
|
||||
private val tokener = LoginToken.makeTokener(tokenerSecret, 10 minutes)
|
||||
}
|
||||
|
|
|
@ -3,58 +3,64 @@ package lila.security
|
|||
import scala.concurrent.duration.{ span => _, _ }
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import play.api.libs.ws.{ WS, WSAuthScheme }
|
||||
import play.api.Play.current
|
||||
import io.methvin.play.autoconfig._
|
||||
import play.api.libs.ws.{ WSClient, WSAuthScheme }
|
||||
import scalatags.Text.all._
|
||||
|
||||
import lila.common.String.html.escapeHtml
|
||||
import lila.common.String.html.nl2brUnsafe
|
||||
import lila.common.config.Secret
|
||||
import lila.common.String.html.{ escapeHtml, nl2brUnsafe }
|
||||
import lila.common.{ Lang, EmailAddress }
|
||||
import lila.i18n.I18nKeys.{ emails => trans }
|
||||
|
||||
final class Mailgun(
|
||||
apiUrl: String,
|
||||
apiKey: String,
|
||||
from: String,
|
||||
replyTo: String,
|
||||
system: ActorSystem
|
||||
) {
|
||||
ws: WSClient,
|
||||
config: Mailgun.Config
|
||||
)(implicit system: ActorSystem) {
|
||||
|
||||
def send(msg: Mailgun.Message): Funit =
|
||||
if (apiUrl.isEmpty) {
|
||||
if (config.apiUrl.isEmpty) {
|
||||
println(msg, "No mailgun API URL")
|
||||
funit
|
||||
} else {
|
||||
lila.mon.email.actions.send()
|
||||
WS.url(s"$apiUrl/messages").withAuth("api", apiKey, WSAuthScheme.BASIC).post(Map(
|
||||
"from" -> Seq(msg.from | from),
|
||||
"to" -> Seq(msg.to.value),
|
||||
"h:Reply-To" -> Seq(msg.replyTo | replyTo),
|
||||
"o:tag" -> msg.tag.toSeq,
|
||||
"subject" -> Seq(msg.subject),
|
||||
"text" -> Seq(msg.text)
|
||||
) ++ msg.htmlBody.?? { body =>
|
||||
Map("html" -> Seq(Mailgun.html.wrap(msg.subject, body).render))
|
||||
}).void addFailureEffect {
|
||||
case e: java.net.ConnectException => lila.mon.http.mailgun.timeout()
|
||||
case _ =>
|
||||
} recoverWith {
|
||||
case e if msg.retriesLeft > 0 => {
|
||||
lila.mon.email.actions.retry()
|
||||
akka.pattern.after(15 seconds, system.scheduler) {
|
||||
send(msg.copy(retriesLeft = msg.retriesLeft - 1))
|
||||
ws.url(s"${config.apiUrl}/messages")
|
||||
.withAuth("api", config.apiKey.value, WSAuthScheme.BASIC).post(Map(
|
||||
"from" -> Seq(msg.from | config.sender),
|
||||
"to" -> Seq(msg.to.value),
|
||||
"h:Reply-To" -> Seq(msg.replyTo | config.replyTo),
|
||||
"o:tag" -> msg.tag.toSeq,
|
||||
"subject" -> Seq(msg.subject),
|
||||
"text" -> Seq(msg.text)
|
||||
) ++ msg.htmlBody.?? { body =>
|
||||
Map("html" -> Seq(Mailgun.html.wrap(msg.subject, body).render))
|
||||
}).void addFailureEffect {
|
||||
case e: java.net.ConnectException => lila.mon.http.mailgun.timeout()
|
||||
case _ =>
|
||||
} recoverWith {
|
||||
case e if msg.retriesLeft > 0 => {
|
||||
lila.mon.email.actions.retry()
|
||||
akka.pattern.after(15 seconds, system.scheduler) {
|
||||
send(msg.copy(retriesLeft = msg.retriesLeft - 1))
|
||||
}
|
||||
}
|
||||
case e => {
|
||||
lila.mon.email.actions.fail()
|
||||
fufail(e)
|
||||
}
|
||||
}
|
||||
case e => {
|
||||
lila.mon.email.actions.fail()
|
||||
fufail(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Mailgun {
|
||||
|
||||
case class Config(
|
||||
@ConfigName("api.url") apiUrl: String,
|
||||
@ConfigName("api.key") apiKey: Secret,
|
||||
sender: String,
|
||||
@ConfigName("reply_to") replyTo: String
|
||||
)
|
||||
implicit val configLoader = AutoConfig.loader[Config]
|
||||
|
||||
case class Message(
|
||||
to: EmailAddress,
|
||||
subject: String,
|
||||
|
|
|
@ -3,13 +3,15 @@ package lila.security
|
|||
import scalatags.Text.all._
|
||||
|
||||
import lila.common.{ Lang, EmailAddress }
|
||||
import lila.common.config._
|
||||
import lila.i18n.I18nKeys.{ emails => trans }
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
final class PasswordReset(
|
||||
mailgun: Mailgun,
|
||||
baseUrl: String,
|
||||
tokenerSecret: String
|
||||
userRepo: UserRepo,
|
||||
baseUrl: BaseUrl,
|
||||
tokenerSecret: Secret
|
||||
) {
|
||||
|
||||
import Mailgun.html._
|
||||
|
@ -42,10 +44,10 @@ ${Mailgun.txt.serviceNote}
|
|||
}
|
||||
|
||||
def confirm(token: String): Fu[Option[User]] =
|
||||
tokener read token flatMap { _ ?? UserRepo.byId }
|
||||
tokener read token flatMap { _ ?? userRepo.byId }
|
||||
|
||||
private val tokener = new StringToken[User.ID](
|
||||
secret = tokenerSecret,
|
||||
getCurrentValue = id => UserRepo getPasswordHash id map (~_)
|
||||
getCurrentValue = id => userRepo getPasswordHash id map (~_)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ final class PrintBan(coll: Coll) {
|
|||
} >> loadFromDb
|
||||
|
||||
private def loadFromDb: Funit =
|
||||
coll.distinct[String, Set]("_id", none).map { hashes =>
|
||||
coll.distinctEasy[String, Set]("_id", $empty).map { hashes =>
|
||||
current = hashes
|
||||
lila.mon.security.firewall.prints(hashes.size)
|
||||
}
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
package lila.security
|
||||
|
||||
import play.api.libs.ws.WS
|
||||
import play.api.mvc.RequestHeader
|
||||
import play.api.Play.current
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.ws.WSClient
|
||||
import play.api.mvc.RequestHeader
|
||||
import io.methvin.play.autoconfig._
|
||||
|
||||
import lila.common.HTTPRequest
|
||||
import lila.common.config.Secret
|
||||
|
||||
trait Recaptcha {
|
||||
|
||||
def verify(response: String, req: RequestHeader): Fu[Boolean]
|
||||
}
|
||||
|
||||
private object Recaptcha {
|
||||
|
||||
case class Config(
|
||||
endpoint: String,
|
||||
@ConfigName("public_key") publicKey: String,
|
||||
@ConfigName("private_key") privateKey: Secret,
|
||||
enabled: Boolean,
|
||||
netDomain: String
|
||||
) {
|
||||
def public = RecaptchaPublicConfig(publicKey, enabled)
|
||||
}
|
||||
implicit val configLoader = AutoConfig.loader[Config]
|
||||
}
|
||||
|
||||
object RecaptchaSkip extends Recaptcha {
|
||||
|
||||
def verify(response: String, req: RequestHeader) = fuTrue
|
||||
|
||||
}
|
||||
|
||||
final class RecaptchaGoogle(
|
||||
endpoint: String,
|
||||
privateKey: String,
|
||||
lichessHostname: String
|
||||
ws: WSClient,
|
||||
config: Recaptcha.Config
|
||||
) extends Recaptcha {
|
||||
|
||||
private case class Response(
|
||||
|
@ -31,18 +46,18 @@ final class RecaptchaGoogle(
|
|||
private implicit val responseReader = Json.reads[Response]
|
||||
|
||||
def verify(response: String, req: RequestHeader): Fu[Boolean] = {
|
||||
WS.url(endpoint).post(Map(
|
||||
"secret" -> Seq(privateKey),
|
||||
"response" -> Seq(response),
|
||||
"remoteip" -> Seq(HTTPRequest lastRemoteAddress req value)
|
||||
ws.url(config.endpoint).post(Map(
|
||||
"secret" -> config.privateKey.value,
|
||||
"response" -> response,
|
||||
"remoteip" -> HTTPRequest.lastRemoteAddress(req).value
|
||||
)) flatMap {
|
||||
case res if res.status == 200 =>
|
||||
res.json.validate[Response] match {
|
||||
case JsSuccess(res, _) => fuccess {
|
||||
res.success && res.hostname == lichessHostname
|
||||
res.success && res.hostname == config.netDomain
|
||||
}
|
||||
case JsError(err) =>
|
||||
fufail(s"$err ${~res.body.lines.toList.headOption}")
|
||||
fufail(s"$err ${~res.body.linesIterator.to(LazyList).headOption}")
|
||||
}
|
||||
case res => fufail(s"${res.status} ${res.body}")
|
||||
} recover {
|
||||
|
|
|
@ -6,8 +6,8 @@ import play.api.data._
|
|||
import play.api.data.Forms._
|
||||
import play.api.data.validation.{ Constraint, Valid => FormValid, Invalid, ValidationError }
|
||||
import play.api.mvc.RequestHeader
|
||||
import reactivemongo.api.ReadPreference
|
||||
import reactivemongo.api.bson._
|
||||
import reactivemongo.api.ReadPreference
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.{ ApiVersion, IpAddress, EmailAddress, HTTPRequest }
|
||||
|
@ -18,8 +18,10 @@ import lila.user.{ User, UserRepo }
|
|||
import User.LoginCandidate
|
||||
|
||||
final class SecurityApi(
|
||||
userRepo: UserRepo,
|
||||
coll: Coll,
|
||||
firewall: Firewall,
|
||||
store: Store,
|
||||
geoIP: GeoIP,
|
||||
authenticator: lila.user.Authenticator,
|
||||
emailValidator: EmailAddressValidator,
|
||||
|
@ -69,25 +71,25 @@ final class SecurityApi(
|
|||
}
|
||||
|
||||
def saveAuthentication(userId: User.ID, apiVersion: Option[ApiVersion])(implicit req: RequestHeader): Fu[String] =
|
||||
UserRepo mustConfirmEmail userId flatMap {
|
||||
userRepo mustConfirmEmail userId flatMap {
|
||||
case true => fufail(SecurityApi MustConfirmEmail userId)
|
||||
case false =>
|
||||
val sessionId = Random secureString 22
|
||||
Store.save(sessionId, userId, req, apiVersion, up = true, fp = none) inject sessionId
|
||||
store.save(sessionId, userId, req, apiVersion, up = true, fp = none) inject sessionId
|
||||
}
|
||||
|
||||
def saveSignup(userId: User.ID, apiVersion: Option[ApiVersion], fp: Option[FingerPrint])(implicit req: RequestHeader): Funit = {
|
||||
val sessionId = Random secureString 22
|
||||
Store.save(s"SIG-$sessionId", userId, req, apiVersion, up = false, fp = fp)
|
||||
store.save(s"SIG-$sessionId", userId, req, apiVersion, up = false, fp = fp)
|
||||
}
|
||||
|
||||
def restoreUser(req: RequestHeader): Fu[Option[FingerPrintedUser]] =
|
||||
firewall.accepts(req) ?? {
|
||||
reqSessionId(req) ?? { sessionId =>
|
||||
Store userIdAndFingerprint sessionId flatMap {
|
||||
store userIdAndFingerprint sessionId flatMap {
|
||||
_ ?? { d =>
|
||||
if (d.isOld) Store.setDateToNow(sessionId)
|
||||
UserRepo byId d.user map { _ map { FingerPrintedUser(_, d.fp) } }
|
||||
if (d.isOld) store.setDateToNow(sessionId)
|
||||
userRepo byId d.user map { _ map { FingerPrintedUser(_, d.fp) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,17 +106,17 @@ final class SecurityApi(
|
|||
}
|
||||
|
||||
def locatedOpenSessions(userId: User.ID, nb: Int): Fu[List[LocatedSession]] =
|
||||
Store.openSessions(userId, nb) map {
|
||||
store.openSessions(userId, nb) map {
|
||||
_.map { session =>
|
||||
LocatedSession(session, geoIP(session.ip))
|
||||
}
|
||||
}
|
||||
|
||||
def dedup(userId: User.ID, req: RequestHeader): Funit =
|
||||
reqSessionId(req) ?? { Store.dedup(userId, _) }
|
||||
reqSessionId(req) ?? { store.dedup(userId, _) }
|
||||
|
||||
def setFingerPrint(req: RequestHeader, fp: FingerPrint): Fu[Option[FingerHash]] =
|
||||
reqSessionId(req) ?? { Store.setFingerPrint(_, fp) map some }
|
||||
reqSessionId(req) ?? { store.setFingerPrint(_, fp) map some }
|
||||
|
||||
val sessionIdKey = "sessionId"
|
||||
|
||||
|
@ -123,24 +125,22 @@ final class SecurityApi(
|
|||
|
||||
def userIdsSharingIp = userIdsSharingField("ip") _
|
||||
|
||||
def recentByIpExists(ip: IpAddress): Fu[Boolean] = Store recentByIpExists ip
|
||||
def recentByIpExists(ip: IpAddress): Fu[Boolean] = store recentByIpExists ip
|
||||
|
||||
def recentByPrintExists(fp: FingerPrint): Fu[Boolean] = Store recentByPrintExists fp
|
||||
def recentByPrintExists(fp: FingerPrint): Fu[Boolean] = store recentByPrintExists fp
|
||||
|
||||
private def userIdsSharingField(field: String)(userId: User.ID): Fu[List[User.ID]] =
|
||||
coll.distinctWithReadPreference[User.ID, List](
|
||||
coll.secondaryPreferred.distinctEasy[User.ID, List](
|
||||
field,
|
||||
$doc("user" -> userId, field $exists true).some,
|
||||
readPreference = ReadPreference.secondaryPreferred
|
||||
$doc("user" -> userId, field $exists true)
|
||||
).flatMap {
|
||||
case Nil => fuccess(Nil)
|
||||
case values => coll.distinctWithReadPreference[User.ID, List](
|
||||
case values => coll.secondaryPreferred.distinctEasy[User.ID, List](
|
||||
"user",
|
||||
$doc(
|
||||
field $in values,
|
||||
"user" $ne userId
|
||||
).some,
|
||||
ReadPreference.secondaryPreferred
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -149,7 +149,7 @@ final class SecurityApi(
|
|||
def recentUserIdsByIp(ip: IpAddress) = recentUserIdsByField("ip")(ip.value)
|
||||
|
||||
def shareIpOrPrint(u1: User.ID, u2: User.ID): Fu[Boolean] =
|
||||
Store.ipsAndFps(List(u1, u2), max = 100) map { ipsAndFps =>
|
||||
store.ipsAndFps(List(u1, u2), max = 100) map { ipsAndFps =>
|
||||
val u1s: Set[String] = ipsAndFps.filter(_.user == u1).flatMap { x =>
|
||||
List(x.ip.value, ~x.fp)
|
||||
}.toSet
|
||||
|
@ -161,15 +161,15 @@ final class SecurityApi(
|
|||
}
|
||||
|
||||
def printUas(fh: FingerHash): Fu[List[String]] =
|
||||
coll.distinct[String, List]("ua", $doc("fp" -> fh.value).some)
|
||||
coll.distinctEasy[String, List]("ua", $doc("fp" -> fh.value))
|
||||
|
||||
private def recentUserIdsByField(field: String)(value: String): Fu[List[User.ID]] =
|
||||
coll.distinct[User.ID, List](
|
||||
coll.distinctEasy[User.ID, List](
|
||||
"user",
|
||||
$doc(
|
||||
field -> value,
|
||||
"date" $gt DateTime.now.minusYears(1)
|
||||
).some
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package lila.security
|
||||
|
||||
import io.methvin.play.autoconfig._
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
import lila.common.config._
|
||||
import lila.common.EmailAddress
|
||||
|
||||
private object SecurityConfig {
|
||||
|
||||
case class Root(
|
||||
collection: Collection,
|
||||
floodDuration: FiniteDuration,
|
||||
geoIP: GeoIP.Config,
|
||||
@ConfigName("password_reset.secret") passwordResetSecret: Secret,
|
||||
emailConfirm: EmailConfirm,
|
||||
@ConfigName("email_change.secret") emailChangeSecret: Secret,
|
||||
@ConfigName("login_token.secret") loginTokenSecret: Secret,
|
||||
tor: Tor,
|
||||
@ConfigName("disposable_email") disposableEmail: DisposableEmail,
|
||||
@ConfigName("dns_api") dnsApi: DnsApi,
|
||||
@ConfigName("check_mail_api") checkMail: CheckMail,
|
||||
recaptcha: Recaptcha.Config,
|
||||
mailgun: Mailgun.Config,
|
||||
net: NetConfig,
|
||||
@ConfigName("ipintel.email") ipIntelEmail: EmailAddress
|
||||
)
|
||||
|
||||
case class Collection(
|
||||
security: CollName,
|
||||
printBan: CollName,
|
||||
firewall: CollName
|
||||
)
|
||||
implicit val collectionLoader = AutoConfig.loader[Collection]
|
||||
|
||||
case class EmailConfirm(
|
||||
enabled: Boolean,
|
||||
secret: Secret,
|
||||
cookie: String
|
||||
)
|
||||
implicit val emailConfirmLoader = AutoConfig.loader[EmailConfirm]
|
||||
|
||||
case class Tor(
|
||||
@ConfigName("provider_url") providerUrl: String,
|
||||
@ConfigName("refresh_delay") refreshDelay: FiniteDuration
|
||||
)
|
||||
implicit val torLoader = AutoConfig.loader[Tor]
|
||||
|
||||
case class DisposableEmail(
|
||||
@ConfigName("provider_url") providerUrl: String,
|
||||
@ConfigName("refresh_delay") refreshDelay: FiniteDuration
|
||||
)
|
||||
implicit val disposableLoader = AutoConfig.loader[DisposableEmail]
|
||||
|
||||
case class DnsApi(
|
||||
url: String,
|
||||
timeout: FiniteDuration
|
||||
)
|
||||
implicit val dnsLoader = AutoConfig.loader[DnsApi]
|
||||
|
||||
case class CheckMail(
|
||||
url: String,
|
||||
key: Secret
|
||||
)
|
||||
implicit val checkMailLoader = AutoConfig.loader[CheckMail]
|
||||
|
||||
implicit val loader = AutoConfig.loader[Root]
|
||||
}
|
|
@ -2,24 +2,21 @@ package lila.security
|
|||
|
||||
import org.joda.time.DateTime
|
||||
import play.api.mvc.RequestHeader
|
||||
import reactivemongo.api.ReadPreference
|
||||
import reactivemongo.api.bson.Macros
|
||||
import reactivemongo.api.ReadPreference
|
||||
|
||||
import lila.common.{ HTTPRequest, ApiVersion, IpAddress }
|
||||
import lila.db.BSON.BSONJodaDateTimeHandler
|
||||
import lila.db.dsl._
|
||||
import lila.user.User
|
||||
|
||||
object Store {
|
||||
private final class Store(coll: Coll) {
|
||||
|
||||
// dirty
|
||||
private val coll = Env.current.storeColl
|
||||
|
||||
private implicit val fingerHashBSONHandler = stringIsoHandler[FingerHash]
|
||||
import Store._
|
||||
|
||||
private val localhost = IpAddress("127.0.0.1")
|
||||
|
||||
private[security] def save(
|
||||
def save(
|
||||
sessionId: String,
|
||||
userId: User.ID,
|
||||
req: RequestHeader,
|
||||
|
@ -39,7 +36,7 @@ object Store {
|
|||
"date" -> DateTime.now,
|
||||
"up" -> up,
|
||||
"api" -> apiVersion.map(_.value),
|
||||
"fp" -> fp.flatMap(FingerHash.apply)
|
||||
"fp" -> fp.flatMap(FingerHash.apply).flatMap(fingerHashBSONHandler.writeOpt)
|
||||
)).void
|
||||
|
||||
private val userIdFingerprintProjection = $doc(
|
||||
|
@ -105,17 +102,6 @@ object Store {
|
|||
case Some(hash) => coll.updateField($doc("_id" -> id), "fp", hash) inject hash
|
||||
}
|
||||
|
||||
case class Info(ip: IpAddress, ua: String, fp: Option[FingerHash], date: DateTime) {
|
||||
def datedIp = Dated(ip, date)
|
||||
def datedFp = fp.map { Dated(_, date) }
|
||||
def datedUa = Dated(ua, date)
|
||||
}
|
||||
private implicit val InfoReader = Macros.reader[Info]
|
||||
|
||||
case class Dated[V](value: V, date: DateTime) extends Ordered[Dated[V]] {
|
||||
def compare(other: Dated[V]) = other.date compareTo date
|
||||
}
|
||||
|
||||
def chronoInfoByUser(userId: User.ID): Fu[List[Info]] =
|
||||
coll.find(
|
||||
$doc(
|
||||
|
@ -123,7 +109,7 @@ object Store {
|
|||
"date" $gt DateTime.now.minusYears(2)
|
||||
),
|
||||
$doc("_id" -> false, "ip" -> true, "ua" -> true, "fp" -> true, "date" -> true)
|
||||
).sort($sort desc "date").list[Info]()
|
||||
).sort($sort desc "date").list[Info]()(InfoReader)
|
||||
|
||||
private case class DedupInfo(_id: String, ip: String, ua: String) {
|
||||
def compositeKey = s"$ip $ua"
|
||||
|
@ -147,16 +133,30 @@ object Store {
|
|||
coll.find($doc("user" $in userIds)).list[IpAndFp](max, ReadPreference.secondaryPreferred)
|
||||
|
||||
private[security] def recentByIpExists(ip: IpAddress): Fu[Boolean] =
|
||||
coll.exists(
|
||||
$doc("ip" -> ip, "date" -> $gt(DateTime.now minusDays 7)),
|
||||
readPreference = ReadPreference.secondaryPreferred
|
||||
coll.secondaryPreferred.exists(
|
||||
$doc("ip" -> ip, "date" -> $gt(DateTime.now minusDays 7))
|
||||
)
|
||||
|
||||
private[security] def recentByPrintExists(fp: FingerPrint): Fu[Boolean] =
|
||||
FingerHash(fp) ?? { hash =>
|
||||
coll.exists(
|
||||
$doc("fp" -> hash, "date" -> $gt(DateTime.now minusDays 7)),
|
||||
readPreference = ReadPreference.secondaryPreferred
|
||||
coll.secondaryPreferred.exists(
|
||||
$doc("fp" -> hash, "date" -> $gt(DateTime.now minusDays 7))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Store {
|
||||
|
||||
case class Dated[V](value: V, date: DateTime) extends Ordered[Dated[V]] {
|
||||
def compare(other: Dated[V]) = other.date compareTo date
|
||||
}
|
||||
|
||||
case class Info(ip: IpAddress, ua: String, fp: Option[FingerHash], date: DateTime) {
|
||||
def datedIp = Dated(ip, date)
|
||||
def datedFp = fp.map { Dated(_, date) }
|
||||
def datedUa = Dated(ua, date)
|
||||
}
|
||||
|
||||
implicit val fingerHashBSONHandler = stringIsoHandler[FingerHash]
|
||||
implicit val InfoReader = Macros.reader[Info]
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package lila.security
|
||||
|
||||
import com.roundeights.hasher.Algo
|
||||
import lila.common.String.base64
|
||||
import org.mindrot.BCrypt
|
||||
|
||||
import lila.common.String.base64
|
||||
import lila.common.config.Secret
|
||||
|
||||
import StringToken.ValueChecker
|
||||
|
||||
private[security] final class StringToken[A](
|
||||
secret: String,
|
||||
secret: Secret,
|
||||
getCurrentValue: A => Fu[String],
|
||||
valueChecker: ValueChecker = ValueChecker.Same,
|
||||
fullHashSize: Int = 14,
|
||||
|
@ -36,7 +38,7 @@ private[security] final class StringToken[A](
|
|||
}
|
||||
}
|
||||
|
||||
private def makeHash(msg: String) = Algo.hmac(secret).sha1(msg).hex take fullHashSize
|
||||
private def makeHash(msg: String) = Algo.hmac(secret.value).sha1(msg).hex take fullHashSize
|
||||
|
||||
private def hashCurrentValue(payload: A) = getCurrentValue(payload) map { v =>
|
||||
currentValueHashSize.fold(v)(makeHash(v) take _)
|
||||
|
|
|
@ -2,16 +2,15 @@ package lila.security
|
|||
|
||||
import lila.common.IpAddress
|
||||
|
||||
import play.api.libs.ws.WS
|
||||
import play.api.Play.current
|
||||
import play.api.libs.ws.WSClient
|
||||
|
||||
final class Tor(providerUrl: String) {
|
||||
final class Tor(ws: WSClient, config: SecurityConfig.Tor) {
|
||||
|
||||
private var ips = Set.empty[IpAddress]
|
||||
|
||||
private[security] def refresh(withIps: Iterable[IpAddress] => Funit): Unit = {
|
||||
WS.url(providerUrl).get() map { res =>
|
||||
ips = res.body.lines.filterNot(_ startsWith "#").map(IpAddress.apply).toSet
|
||||
ws.url(config.providerUrl).get() map { res =>
|
||||
ips = res.body.linesIterator.filterNot(_ startsWith "#").map(IpAddress.apply).toSet
|
||||
withIps(ips)
|
||||
lila.mon.security.tor.node(ips.size)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package lila.security
|
|||
|
||||
import org.joda.time.DateTime
|
||||
import reactivemongo.api.ReadPreference
|
||||
import scala.collection.breakOut
|
||||
|
||||
import lila.common.{ IpAddress, EmailAddress }
|
||||
import lila.db.dsl._
|
||||
|
@ -34,12 +33,18 @@ case class UserSpy(
|
|||
def otherUserIds = otherUsers.map(_.user.id)
|
||||
}
|
||||
|
||||
final class UserSpyApi(firewall: Firewall, geoIP: GeoIP, coll: Coll) {
|
||||
final class UserSpyApi(
|
||||
firewall: Firewall,
|
||||
store: Store,
|
||||
userRepo: UserRepo,
|
||||
geoIP: GeoIP,
|
||||
coll: Coll
|
||||
) {
|
||||
|
||||
import UserSpy._
|
||||
|
||||
def apply(user: User): Fu[UserSpy] = for {
|
||||
infos <- Store.chronoInfoByUser(user.id)
|
||||
infos <- store.chronoInfoByUser(user.id)
|
||||
ips = distinctRecent(infos.map(_.datedIp))
|
||||
prints = distinctRecent(infos.flatMap(_.datedFp))
|
||||
sharingIp <- exploreSimilar("ip")(user)
|
||||
|
@ -54,10 +59,10 @@ final class UserSpyApi(firewall: Firewall, geoIP: GeoIP, coll: Coll) {
|
|||
usersSharingFingerprint = sharingFingerprint
|
||||
)
|
||||
|
||||
private[security] def userHasPrint(u: User): Fu[Boolean] = coll.exists(
|
||||
$doc("user" -> u.id, "fp" $exists true),
|
||||
readPreference = ReadPreference.secondaryPreferred
|
||||
)
|
||||
private[security] def userHasPrint(u: User): Fu[Boolean] =
|
||||
coll.secondaryPreferred.exists(
|
||||
$doc("user" -> u.id, "fp" $exists true)
|
||||
)
|
||||
|
||||
private def exploreSimilar(field: String)(user: User): Fu[Set[User]] =
|
||||
nextValues(field)(user.id) flatMap { nValues =>
|
||||
|
@ -69,34 +74,32 @@ final class UserSpyApi(firewall: Firewall, geoIP: GeoIP, coll: Coll) {
|
|||
$doc("user" -> userId),
|
||||
$doc(field -> true)
|
||||
).list[Bdoc]() map {
|
||||
_.flatMap(_.getAs[Value](field))(breakOut)
|
||||
_.view.flatMap(_.getAsOpt[Value](field)).to(Set)
|
||||
}
|
||||
|
||||
private def nextUsers(field: String)(values: Set[Value], user: User): Fu[Set[User]] =
|
||||
values.nonEmpty ?? {
|
||||
coll.distinctWithReadPreference[String, Set](
|
||||
coll.secondaryPreferred.distinctEasy[String, Set](
|
||||
"user",
|
||||
$doc(
|
||||
field $in values,
|
||||
"user" $ne user.id
|
||||
).some,
|
||||
ReadPreference.secondaryPreferred
|
||||
)
|
||||
) flatMap { userIds =>
|
||||
userIds.nonEmpty ?? (UserRepo byIds userIds) map (_.toSet)
|
||||
userIds.nonEmpty ?? (userRepo byIds userIds) map (_.toSet)
|
||||
}
|
||||
}
|
||||
|
||||
def getUserIdsWithSameIpAndPrint(userId: User.ID): Fu[Set[User.ID]] = for {
|
||||
ips <- nextValues("ip")(userId)
|
||||
fps <- nextValues("fp")(userId)
|
||||
users <- (ips.nonEmpty && fps.nonEmpty) ?? coll.distinctWithReadPreference[User.ID, Set](
|
||||
users <- (ips.nonEmpty && fps.nonEmpty) ?? coll.secondaryPreferred.distinctEasy[User.ID, Set](
|
||||
"user",
|
||||
$doc(
|
||||
"ip" $in ips,
|
||||
"fp" $in fps,
|
||||
"user" $ne userId
|
||||
).some,
|
||||
ReadPreference.secondaryPreferred
|
||||
)
|
||||
)
|
||||
} yield users
|
||||
}
|
||||
|
@ -113,7 +116,7 @@ object UserSpy {
|
|||
all.foldLeft(Map.empty[V, DateTime]) {
|
||||
case (acc, Dated(v, _)) if acc.contains(v) => acc
|
||||
case (acc, Dated(v, date)) => acc + (v -> date)
|
||||
}.map { case (v, date) => Dated(v, date) }(breakOut)
|
||||
}.view.map { case (v, date) => Dated(v, date) }.to(List)
|
||||
|
||||
type Value = String
|
||||
|
||||
|
@ -123,9 +126,9 @@ object UserSpy {
|
|||
def emailValueOf(u: User) = emails.get(u.id).map(_.value)
|
||||
}
|
||||
|
||||
def withMeSortedWithEmails(me: User, others: Set[OtherUser]): Fu[WithMeSortedWithEmails] = {
|
||||
def withMeSortedWithEmails(userRepo: UserRepo, me: User, others: Set[OtherUser]): Fu[WithMeSortedWithEmails] = {
|
||||
val othersList = others.toList
|
||||
lila.user.UserRepo.emailMap(me.id :: othersList.map(_.user.id)) map { emailMap =>
|
||||
userRepo.emailMap(me.id :: othersList.map(_.user.id)) map { emailMap =>
|
||||
WithMeSortedWithEmails(
|
||||
(OtherUser(me, true, true) :: othersList).sortBy(-_.user.createdAt.getMillis),
|
||||
emailMap
|
||||
|
|
|
@ -9,7 +9,7 @@ import scala.concurrent.duration._
|
|||
|
||||
import lila.db.dsl.Coll
|
||||
import lila.common.config._
|
||||
import lila.common.{ CollName, Secret, MaxPerPage, LightUser }
|
||||
import lila.common.{ MaxPerPage, LightUser }
|
||||
|
||||
case class UserConfig(
|
||||
@ConfigName("paginator.max_per_page") paginatorMaxPerPage: MaxPerPage,
|
||||
|
|
|
@ -6,7 +6,7 @@ import javax.crypto.Cipher
|
|||
import javax.crypto.spec.{ IvParameterSpec, SecretKeySpec }
|
||||
import com.roundeights.hasher.Implicits._
|
||||
|
||||
import lila.common.Secret
|
||||
import lila.common.config.Secret
|
||||
|
||||
/**
|
||||
* Encryption for bcrypt hashes.
|
||||
|
|
|
@ -5,7 +5,7 @@ import reactivemongo.api._
|
|||
import reactivemongo.api.bson._
|
||||
import reactivemongo.api.commands.GetLastError
|
||||
|
||||
import lila.common.{ CollName, ApiVersion, EmailAddress, NormalizedEmailAddress }
|
||||
import lila.common.{ ApiVersion, EmailAddress, NormalizedEmailAddress }
|
||||
import lila.db.BSON.BSONJodaDateTimeHandler
|
||||
import lila.db.dsl._
|
||||
import lila.rating.{ Perf, PerfType }
|
||||
|
|
Loading…
Reference in New Issue