From 7d395ed3a5976fe8ed32bf0c2ce3c19715ae49ef Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 4 Nov 2024 12:06:32 +0100 Subject: [PATCH] better validate multiple broadcast source URLs --- modules/relay/src/main/RelayFormat.scala | 5 ++- modules/relay/src/main/RelayRound.scala | 24 ++++++++------ modules/relay/src/main/RelayRoundForm.scala | 36 ++++++++++----------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/modules/relay/src/main/RelayFormat.scala b/modules/relay/src/main/RelayFormat.scala index ffd25da475441..74b1857f471f5 100644 --- a/modules/relay/src/main/RelayFormat.scala +++ b/modules/relay/src/main/RelayFormat.scala @@ -45,9 +45,8 @@ final private class RelayFormatApi( cache.invalidate(url -> proxy) private def guessFormat(url: URL)(using CanProxy): Fu[RelayFormat] = - RelayRound.Sync.Upstream - .Url(url) - .lcc + import RelayRound.Sync.url.* + url.lcc .match case Some(lcc) => looksLikeJson(lcc.indexUrl).flatMapz: diff --git a/modules/relay/src/main/RelayRound.scala b/modules/relay/src/main/RelayRound.scala index d7f2a3bbef5f1..b52b48aacfa16 100644 --- a/modules/relay/src/main/RelayRound.scala +++ b/modules/relay/src/main/RelayRound.scala @@ -128,6 +128,16 @@ object RelayRound: override def toString = upstream.toString object Sync: + + object url: + private val lccRegex = """view\.livechesscloud\.com/?#?([0-9a-f\-]+)/(\d+)""".r.unanchored + extension (url: URL) + def lcc: Option[Lcc] = url.toString match + case lccRegex(id, round) => round.toIntOption.map(Lcc(id, _)) + case _ => none + def looksLikeLcc = url.host.toString.endsWith("livechesscloud.com") + import url.* + enum Upstream: case Url(url: URL) extends Upstream case Urls(urls: List[URL]) extends Upstream @@ -135,14 +145,11 @@ object RelayRound: def asUrl: Option[URL] = this match case Url(url) => url.some case _ => none - def isUrl = asUrl.isDefined - def lcc: Option[Lcc] = asUrl.flatMap: - _.toString match - case lccRegex(id, round) => round.toIntOption.map(Lcc(id, _)) - case _ => none + def isUrl = asUrl.isDefined + def lcc: Option[Lcc] = asUrl.flatMap(_.lcc) def hasLcc = this match - case Url(url) => Sync.looksLikeLcc(url) - case Urls(urls) => urls.exists(Sync.looksLikeLcc) + case Url(url) => url.looksLikeLcc + case Urls(urls) => urls.exists(_.looksLikeLcc) case _ => false def roundId: Option[RelayRoundId] = this match @@ -164,9 +171,6 @@ object RelayRound: def indexUrl = URL.parse(s"http://1.pool.livechesscloud.com/get/$id/round-$round/index.json") def gameUrl(g: Int) = URL.parse(s"http://1.pool.livechesscloud.com/get/$id/round-$round/game-$g.json") - private val lccRegex = """view\.livechesscloud\.com/?#?([0-9a-f\-]+)/(\d+)""".r.unanchored - private def looksLikeLcc(url: URL) = url.toString.contains(".livechesscloud.com/") - trait AndTour: val tour: RelayTour def display: RelayRound diff --git a/modules/relay/src/main/RelayRoundForm.scala b/modules/relay/src/main/RelayRoundForm.scala index da5aa3ecfb747..65c06a5b7ac6b 100644 --- a/modules/relay/src/main/RelayRoundForm.scala +++ b/modules/relay/src/main/RelayRoundForm.scala @@ -10,6 +10,7 @@ import lila.common.Form.{ cleanText, formatter, into, stringIn, LocalDateTimeOrT import lila.core.perm.Granter import lila.relay.RelayRound.Sync import lila.relay.RelayRound.Sync.Upstream +import lila.relay.RelayRound.Sync.url.* import lila.common.Form.PrettyDateTime final class RelayRoundForm(using mode: Mode): @@ -17,9 +18,10 @@ final class RelayRoundForm(using mode: Mode): import RelayRoundForm.* import lila.common.Form.ISOInstantOrTimestamp - private given Formatter[Upstream.Url] = + private given (using Me): Formatter[Upstream.Url] = formatter.stringTryFormatter(str => validateUpstreamUrl(str).map(Upstream.Url.apply), _.url.toString) - private given Formatter[Upstream.Urls] = formatter.stringTryFormatter( + + private given (using Me): Formatter[Upstream.Urls] = formatter.stringTryFormatter( _.linesIterator.toList .map(_.trim) .filter(_.nonEmpty) @@ -42,22 +44,12 @@ final class RelayRoundForm(using mode: Mode): _.ids.mkString(" ") ) - private def lccIsComplete(url: Upstream.Url) = - url.lcc.isDefined || !url.url.host.toString.endsWith("livechesscloud.com") - - def roundMapping(tour: RelayTour)(using Me) = + def roundMapping(tour: RelayTour)(using Me): Mapping[Data] = mapping( - "name" -> cleanText(minLength = 3, maxLength = 80).into[RelayRound.Name], - "caption" -> optional(cleanText(minLength = 3, maxLength = 80).into[RelayRound.Caption]), - "syncSource" -> optional(stringIn(sourceTypes.map(_._1).toSet)), - "syncUrl" -> optional( - of[Upstream.Url] - .verifying("LCC URLs must end with /{round-number}, e.g. /5 for round 5", lccIsComplete) - .verifying( - "Invalid source URL", - u => !u.url.host.toString.endsWith("lichess.org") || Granter(_.Relay) - ) - ), + "name" -> cleanText(minLength = 3, maxLength = 80).into[RelayRound.Name], + "caption" -> optional(cleanText(minLength = 3, maxLength = 80).into[RelayRound.Caption]), + "syncSource" -> optional(stringIn(sourceTypes.map(_._1).toSet)), + "syncUrl" -> optional(of[Upstream.Url]), "syncUrls" -> optional(of[Upstream.Urls]), "syncIds" -> optional(of[Upstream.Ids]), "startsAt" -> optional(LocalDateTimeOrTimestamp(tour.info.timeZoneOrDefault).mapping), @@ -155,9 +147,17 @@ object RelayRoundForm: if !subdomain(host, "chess.com") || url.toString.startsWith("https://api.chess.com/pub") yield url - private def validateUpstreamUrl(s: String)(using Mode): Either[String, URL] = for + private def validateUpstreamUrl(s: String)(using Me, Mode): Either[String, URL] = for url <- cleanUrl(s).toRight("Invalid source URL") url <- if !validSourcePort(url) then Left("The source URL cannot specify a port") else Right(url) + url <- + if url.looksLikeLcc && !url.lcc.isDefined + then Left("LCC URLs must end with /{round-number}, e.g. /5 for round 5") + else Right(url) + url <- + if url.host.toString.endsWith("lichess.org") && !Granter(_.Relay) + then Left("Invalid source URL") + else Right(url) yield url private val validPorts = Set(-1, 80, 443, 8080, 8491)