From 92ef2b9ae9a9d6697c3b79ccdc23e9897ea4944c Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 3 Apr 2018 06:27:40 +0200 Subject: [PATCH] require HTTP/1.1 for chunked responses --- app/controllers/Api.scala | 8 ++-- app/controllers/Export.scala | 50 ++++++++++++----------- app/controllers/Game.scala | 21 ++++++---- app/controllers/Irwin.scala | 4 +- app/controllers/LilaController.scala | 6 +++ app/controllers/Search.scala | 34 +++++++-------- app/controllers/Tv.scala | 20 +++++---- modules/common/src/main/HTTPRequest.scala | 2 + 8 files changed, 84 insertions(+), 61 deletions(-) diff --git a/app/controllers/Api.scala b/app/controllers/Api.scala index 073fee83f4..fa2b373878 100644 --- a/app/controllers/Api.scala +++ b/app/controllers/Api.scala @@ -282,9 +282,11 @@ object Api extends LilaController { } map toApiResult } - def gameStream = Action(parse.tolerantText) { req => - val userIds = req.body.split(',').take(300).toSet map lila.user.User.normalize - Ok.chunked(Env.game.stream.startedByUserIds(userIds)) + def gameStream = Action.async(parse.tolerantText) { req => + RequireHttp11(req) { + val userIds = req.body.split(',').take(300).toSet map lila.user.User.normalize + Ok.chunked(Env.game.stream.startedByUserIds(userIds)).fuccess + } } def activity(name: String) = ApiRequest { implicit ctx => diff --git a/app/controllers/Export.scala b/app/controllers/Export.scala index eccbd91797..09e43d5835 100644 --- a/app/controllers/Export.scala +++ b/app/controllers/Export.scala @@ -49,14 +49,16 @@ object Export extends LilaController { def png(id: String) = Open { implicit ctx => OnlyHumansAndFacebookOrTwitter { - PngRateLimitGlobal("-", msg = s"${HTTPRequest.lastRemoteAddress(ctx.req).value} ${~HTTPRequest.userAgent(ctx.req)}") { - lila.mon.export.png.game() - OptionFuResult(GameRepo game id) { game => - env.pngExport fromGame game map { stream => - Ok.chunked(stream).withHeaders( - CONTENT_TYPE -> "image/png", - CACHE_CONTROL -> "max-age=7200" - ) + RequireHttp11 { + PngRateLimitGlobal("-", msg = s"${HTTPRequest.lastRemoteAddress(ctx.req).value} ${~HTTPRequest.userAgent(ctx.req)}") { + lila.mon.export.png.game() + OptionFuResult(GameRepo game id) { game => + env.pngExport fromGame game map { stream => + Ok.chunked(stream).withHeaders( + CONTENT_TYPE -> "image/png", + CACHE_CONTROL -> "max-age=7200" + ) + } } } } @@ -65,21 +67,23 @@ object Export extends LilaController { def puzzlePng(id: Int) = Open { implicit ctx => OnlyHumansAndFacebookOrTwitter { - PngRateLimitGlobal("-", msg = HTTPRequest.lastRemoteAddress(ctx.req).value) { - lila.mon.export.png.puzzle() - OptionFuResult(Env.puzzle.api.puzzle find id) { puzzle => - env.pngExport( - fen = chess.format.FEN(puzzle.fenAfterInitialMove | puzzle.fen), - lastMove = puzzle.initialMove.uci.some, - check = none, - orientation = puzzle.color.some, - logHint = s"puzzle $id" - ) map { stream => - Ok.chunked(stream).withHeaders( - CONTENT_TYPE -> "image/png", - CACHE_CONTROL -> "max-age=7200" - ) - } + RequireHttp11 { + PngRateLimitGlobal("-", msg = HTTPRequest.lastRemoteAddress(ctx.req).value) { + lila.mon.export.png.puzzle() + OptionFuResult(Env.puzzle.api.puzzle find id) { puzzle => + env.pngExport( + fen = chess.format.FEN(puzzle.fenAfterInitialMove | puzzle.fen), + lastMove = puzzle.initialMove.uci.some, + check = none, + orientation = puzzle.color.some, + logHint = s"puzzle $id" + ) map { stream => + Ok.chunked(stream).withHeaders( + CONTENT_TYPE -> "image/png", + CACHE_CONTROL -> "max-age=7200" + ) + } + } } } } diff --git a/app/controllers/Game.scala b/app/controllers/Game.scala index d8e1c6f11a..478ec96ba0 100644 --- a/app/controllers/Game.scala +++ b/app/controllers/Game.scala @@ -3,6 +3,7 @@ package controllers import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import scala.concurrent.duration._ +import play.api.mvc.RequestHeader import lila.app._ import lila.game.{ GameRepo, Game => GameModel } @@ -36,7 +37,7 @@ object Game extends LilaController { err => Env.security.forms.anyCaptcha map { captcha => BadRequest(html.game.export(err, captcha)) }, - _ => fuccess(streamGamesPgn(me, since = none, until = none)) + _ => streamGamesPgn(req, me, since = none, until = none) ) } @@ -44,7 +45,7 @@ object Game extends LilaController { val since = getLong("since", req) map { ts => new DateTime(ts) } val until = getLong("until", req) map { ts => new DateTime(ts) } val max = getInt("max", req) - fuccess(streamGamesPgn(me, since, until, max)) + streamGamesPgn(req, me, since, until, max) } private val ExportRateLimitPerUser = new lila.memo.RateLimit[lila.user.User.ID]( @@ -54,13 +55,15 @@ object Game extends LilaController { key = "game_export.user" ) - private def streamGamesPgn(user: lila.user.User, since: Option[DateTime], until: Option[DateTime], max: Option[Int] = None) = - ExportRateLimitPerUser(user.id, cost = 1) { - val date = (DateTimeFormat forPattern "yyyy-MM-dd") print new DateTime - Ok.chunked(Env.api.pgnDump.exportUserGames(user.id, since, until, max | Int.MaxValue)).withHeaders( - CONTENT_TYPE -> pgnContentType, - CONTENT_DISPOSITION -> ("attachment; filename=" + s"lichess_${user.username}_$date.pgn") - ) + private def streamGamesPgn(req: RequestHeader, user: lila.user.User, since: Option[DateTime], until: Option[DateTime], max: Option[Int] = None) = + RequireHttp11(req) { + ExportRateLimitPerUser(user.id, cost = 1) { + val date = (DateTimeFormat forPattern "yyyy-MM-dd") print new DateTime + Ok.chunked(Env.api.pgnDump.exportUserGames(user.id, since, until, max | Int.MaxValue)).withHeaders( + CONTENT_TYPE -> pgnContentType, + CONTENT_DISPOSITION -> ("attachment; filename=" + s"lichess_${user.username}_$date.pgn") + ) + } fuccess } private[controllers] def preloadUsers(game: GameModel): Funit = diff --git a/app/controllers/Irwin.scala b/app/controllers/Irwin.scala index 194a249043..67c3b18316 100644 --- a/app/controllers/Irwin.scala +++ b/app/controllers/Irwin.scala @@ -54,7 +54,9 @@ object Irwin extends LilaController { def eventStream = Open { implicit ctx => ModExternalBot { - Ok.chunked(Env.irwin.stream.enumerator).fuccess + RequireHttp11 { + Ok.chunked(Env.irwin.stream.enumerator).fuccess + } } } diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index 3ebef21870..8dde674f18 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -410,6 +410,12 @@ private[controllers] trait LilaController else if (HTTPRequest isBot ctx.req) fuccess(NotFound) else result + protected def RequireHttp11(result: => Fu[Result])(implicit ctx: lila.api.Context): Fu[Result] = + RequireHttp11(ctx.req)(result) + protected def RequireHttp11(req: RequestHeader)(result: => Fu[Result]): Fu[Result] = + if (HTTPRequest isHttp10 req) BadRequest("Requires HTTP 1.1").fuccess + else result + private val jsonGlobalErrorRenamer = { import play.api.libs.json._ __.json update ( diff --git a/app/controllers/Search.scala b/app/controllers/Search.scala index 4cd52e9909..3058a2266d 100644 --- a/app/controllers/Search.scala +++ b/app/controllers/Search.scala @@ -81,23 +81,25 @@ object Search extends LilaController { def export = OpenBody { implicit ctx => NotForBots { - implicit def req = ctx.body - searchForm.bindFromRequest.fold( - failure => Env.game.cached.nbTotal map { nbGames => - Ok(html.search.index(failure, none, nbGames)) - }, - data => data.nonEmptyQuery ?? { query => - env.api.ids(query, 5000) map { ids => - import org.joda.time.DateTime - import org.joda.time.format.DateTimeFormat - val date = (DateTimeFormat forPattern "yyyy-MM-dd") print DateTime.now - Ok.chunked(Env.api.pgnDump exportGamesFromIds ids).withHeaders( - CONTENT_TYPE -> pgnContentType, - CONTENT_DISPOSITION -> ("attachment; filename=" + s"lichess_search_$date.pgn") - ) + RequireHttp11 { + implicit def req = ctx.body + searchForm.bindFromRequest.fold( + failure => Env.game.cached.nbTotal map { nbGames => + Ok(html.search.index(failure, none, nbGames)) + }, + data => data.nonEmptyQuery ?? { query => + env.api.ids(query, 5000) map { ids => + import org.joda.time.DateTime + import org.joda.time.format.DateTimeFormat + val date = (DateTimeFormat forPattern "yyyy-MM-dd") print DateTime.now + Ok.chunked(Env.api.pgnDump exportGamesFromIds ids).withHeaders( + CONTENT_TYPE -> pgnContentType, + CONTENT_DISPOSITION -> ("attachment; filename=" + s"lichess_search_$date.pgn") + ) + } } - } - ) + ) + } } } } diff --git a/app/controllers/Tv.scala b/app/controllers/Tv.scala index f43fbd2893..5c346754f9 100644 --- a/app/controllers/Tv.scala +++ b/app/controllers/Tv.scala @@ -70,15 +70,17 @@ object Tv extends LilaController { } } - def feed = Action.async { - import makeTimeout.short - import akka.pattern.ask - import lila.round.TvBroadcast - import play.api.libs.EventSource - Env.round.tvBroadcast ? TvBroadcast.GetEnumerator mapTo - manifest[TvBroadcast.EnumeratorType] map { enum => - Ok.chunked(enum &> EventSource()).as("text/event-stream") - } + def feed = Action.async { req => + RequireHttp11(req) { + import makeTimeout.short + import akka.pattern.ask + import lila.round.TvBroadcast + import play.api.libs.EventSource + Env.round.tvBroadcast ? TvBroadcast.GetEnumerator mapTo + manifest[TvBroadcast.EnumeratorType] map { enum => + Ok.chunked(enum &> EventSource()).as("text/event-stream") + } + } } def embed = Action { req => diff --git a/modules/common/src/main/HTTPRequest.scala b/modules/common/src/main/HTTPRequest.scala index c8c567fddc..57920f3585 100644 --- a/modules/common/src/main/HTTPRequest.scala +++ b/modules/common/src/main/HTTPRequest.scala @@ -71,4 +71,6 @@ object HTTPRequest { def printClient(req: RequestHeader) = s"${lastRemoteAddress(req)} origin:${~origin(req)} referer:${~referer(req)} ua:${~userAgent(req)}" def isOAuth(req: RequestHeader) = req.headers.toMap.contains(HeaderNames.AUTHORIZATION) + + def isHttp10(req: RequestHeader) = req.version == "HTTP/1.0" }