sign picfit requests to prevent DoSing

ublog
Thibault Duplessis 2021-09-01 11:08:24 +02:00
parent a79af756c0
commit 124eb61192
6 changed files with 44 additions and 15 deletions

View File

@ -26,7 +26,7 @@ trait AssetHelper { self: I18nHelper with SecurityHelper =>
def dbImageUrl(path: String) = s"$assetBaseUrl/image/$path"
def picfitUrl(id: lila.memo.PicfitImage.Id) = new lila.memo.PicfitUrl(env.net.picfitEndpoint, id)
lazy val picfitUrl = new lila.memo.PicfitUrl(env.net.picfitEndpoint, env.net.picfitSecretKey)
def cssTag(name: String)(implicit ctx: Context): Frag =
cssTagWithTheme(name, ctx.currentBg)

View File

@ -80,7 +80,7 @@ object post {
val (w, h) = (600, 300)
post.image match {
case Some(image) =>
baseImg(src := picfitUrl(image).thumbnail(w, h))
baseImg(src := picfitUrl.thumbnail(image, w, h))
case _ =>
div(cls := "ublog-post-image-default")
}
@ -99,7 +99,7 @@ object post {
}
def imageUrlOf(post: UblogPost, height: Int = defaultImageHeight) = post.image map { i =>
picfitUrl(i).resize(Right(height))
picfitUrl.resize(i, Right(height))
}
private val baseImg = img(cls := "ublog-post-image")

View File

@ -18,6 +18,7 @@ net {
http.log = true
stage.banner = false
picfitEndpoint = ${memo.picfit.endpointGet}
picfitSecretKey = ${memo.picfit.secretKey}
}
play {
application.loader = "lila.app.AppLoader"
@ -390,6 +391,7 @@ memo {
collection = picfit_image
endpointGet = "http://127.0.0.1:3001"
endpointPost = "http://127.0.0.1:3001"
secretKey = "qix8rozsRE6Rsw5uvBjwJUCFfQhyaKbR" # prod uses a different key
}
}
redis {

View File

@ -43,7 +43,8 @@ object config {
crawlable: Boolean,
@ConfigName("ratelimit") rateLimit: RateLimit,
email: EmailAddress,
picfitEndpoint: String
picfitEndpoint: String,
picfitSecretKey: Secret
) {
def isProd = domain == prodDomain
}

View File

@ -9,9 +9,14 @@ import lila.common.config._
final class MemoConfig(
@ConfigName("collection.cache") val cacheColl: CollName,
@ConfigName("collection.config") val configColl: CollName,
@ConfigName("picfit.collection") val picfitColl: CollName,
@ConfigName("picfit.endpointGet") val picfitEndpointGet: String,
@ConfigName("picfit.endpointPost") val picfitEndpointPost: String
val picfit: PicfitConfig
)
final class PicfitConfig(
val collection: CollName,
val endpointGet: String,
val endpointPost: String,
val secretKey: Secret
)
@Module
@ -22,7 +27,8 @@ final class Env(
ws: play.api.libs.ws.StandaloneWSClient
)(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem) {
private val config = appConfig.get[MemoConfig]("memo")(AutoConfig.loader)
implicit val picfitLoader = AutoConfig.loader[PicfitConfig]
private val config = appConfig.get[MemoConfig]("memo")(AutoConfig.loader)
lazy val configStore = wire[ConfigStore.Builder]
@ -34,5 +40,5 @@ final class Env(
lazy val mongoRateLimitApi = wire[MongoRateLimitApi]
lazy val picfitApi = new PicfitApi(db(config.picfitColl), ws, endpoint = config.picfitEndpointPost)
lazy val picfitApi = new PicfitApi(db(config.picfit.collection), ws, config.picfit)
}

View File

@ -6,9 +6,12 @@ import org.joda.time.DateTime
import play.api.libs.ws.StandaloneWSClient
import play.api.mvc.MultipartFormData
import reactivemongo.api.bson.Macros
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext
import lila.common.config
import lila.db.dsl._
import com.github.blemale.scaffeine.LoadingCache
case class PicfitImage(
_id: PicfitImage.Id,
@ -33,7 +36,9 @@ object PicfitImage {
implicit val imageBSONHandler = Macros.handler[PicfitImage]
}
final class PicfitApi(coll: Coll, ws: StandaloneWSClient, endpoint: String)(implicit ec: ExecutionContext) {
final class PicfitApi(coll: Coll, ws: StandaloneWSClient, config: PicfitConfig)(implicit
ec: ExecutionContext
) {
import PicfitApi._
private val uploadMaxBytes = uploadMaxMb * 1024 * 1024
@ -80,7 +85,7 @@ final class PicfitApi(coll: Coll, ws: StandaloneWSClient, endpoint: String)(impl
fileSize = from.fileSize
)
val source: Source[Part, _] = Source(part :: List())
ws.url(s"$endpoint/upload")
ws.url(s"${config.endpointPost}/upload")
.post(source)
.flatMap {
case res if res.status != 200 => fufail(res.statusText)
@ -92,7 +97,7 @@ final class PicfitApi(coll: Coll, ws: StandaloneWSClient, endpoint: String)(impl
}
def delete(image: PicfitImage): Funit =
ws.url(s"$endpoint/${image.id}").delete().flatMap {
ws.url(s"${config.endpointPost}/${image.id}").delete().flatMap {
case res if res.status != 200 =>
logger
.branch("picfit")
@ -123,11 +128,12 @@ object PicfitApi {
}
}
final class PicfitUrl(endpoint: String, id: PicfitImage.Id) {
final class PicfitUrl(endpoint: String, secretKey: config.Secret) {
// This operation will able you to resize the image to the specified width and height.
// Preserves the aspect ratio
def resize(
id: PicfitImage.Id,
size: Either[Int, Int], // either the width or the height! the other one will be preserved
upscale: Boolean = true
) = display(id, "resize")(
@ -140,6 +146,7 @@ final class PicfitUrl(endpoint: String, id: PicfitImage.Id) {
// crops it to the specified width and height and returns the transformed image.
// Preserves the aspect ratio
def thumbnail(
id: PicfitImage.Id,
width: Int,
height: Int,
upscale: Boolean = true
@ -149,6 +156,19 @@ final class PicfitUrl(endpoint: String, id: PicfitImage.Id) {
width: Int,
height: Int,
upscale: Boolean
) =
s"$endpoint/display?path=$id&op=$operation&w=$width&h=$height&upscale=${upscale ?? 1}"
) = {
// parameters must be given in alphabetical order for the signature to work (!)
val queryString = s"h=$height&op=$operation&path=$id&upscale=${upscale ?? 1}&w=$width"
s"$endpoint/display?${signQueryString(queryString)}"
}
private object signQueryString {
private val signer = com.roundeights.hasher.Algo hmac secretKey.value
private val cache: LoadingCache[String, String] =
CacheApi.scaffeineNoScheduler
.expireAfterWrite(5 minutes)
.build { qs => signer.sha1(qs).hex }
def apply(qs: String) = s"$qs&sig=${cache get qs}"
}
}