diff --git a/.gitignore b/.gitignore index 59e2ba53a4..e2d6563394 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ logs project/project project/target public/trans +public/compiled serve/ serve/README target diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index 121aaf5f38..256830b06c 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -83,6 +83,9 @@ trait LilaController protected def JsonIOk(map: IO[Map[String, Any]]) = JsonOk(map.unsafePerformIO) + protected def JsIOk(js: IO[String], headers: (String, String)*) = + Ok(js.unsafePerformIO) as JAVASCRIPT withHeaders (headers: _*) + protected def ValidOk(valid: Valid[Unit]) = valid.fold( e ⇒ BadRequest(e.shows), _ ⇒ Ok("ok") diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index e38789bc1c..50122b87ec 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -24,6 +24,7 @@ object Round extends LilaController with TheftPrevention with RoundEventPerforme private def userRepo = env.user.userRepo private def analyser = env.analyse.analyser private def tournamentRepo = env.tournament.repo + private def gameJs = env.game.gameJs def websocketWatcher(gameId: String, color: String) = WebSocket.async[JsValue] { req ⇒ implicit val ctx = reqToCtx(req) @@ -32,16 +33,22 @@ object Round extends LilaController with TheftPrevention with RoundEventPerforme color, getInt("version"), get("sri"), + get("tk"), ctx).unsafePerformIO } def websocketPlayer(fullId: String) = WebSocket.async[JsValue] { req ⇒ implicit val ctx = reqToCtx(req) - socket.joinPlayer( - fullId, - getInt("version"), - get("sri"), - ctx).unsafePerformIO + socket.joinPlayer( + fullId, + getInt("version"), + get("sri"), + get("tk"), + ctx).unsafePerformIO + } + + def signedJs(gameId: String) = Open { implicit ctx ⇒ + JsIOk(gameRepo token gameId map gameJs.sign, CACHE_CONTROL -> "max-age=3600") } def player(fullId: String) = Open { implicit ctx ⇒ diff --git a/app/core/Settings.scala b/app/core/Settings.scala index 06627c8e18..c2974fa031 100644 --- a/app/core/Settings.scala +++ b/app/core/Settings.scala @@ -25,6 +25,7 @@ final class Settings(config: Config, val IsDev: Boolean) { val GamePaginatorMaxPerPage = getInt("game.paginator.max_per_page") val GameCollectionGame = getString("game.collection.game") val GameCollectionPgn = getString("game.collection.pgn") + val GameJsPath = getString("game.js_path") val SearchESHost = getString("search.elasticsearch.host") val SearchESPort = getInt("search.elasticsearch.port") diff --git a/app/game/DbGame.scala b/app/game/DbGame.scala index 400f48fcae..574d31e8f1 100644 --- a/app/game/DbGame.scala +++ b/app/game/DbGame.scala @@ -15,6 +15,7 @@ import scala.math.min case class DbGame( id: String, + token: String, whitePlayer: DbPlayer, blackPlayer: DbPlayer, status: Status, @@ -322,6 +323,7 @@ case class DbGame( def encode = RawDbGame( id = id, + tk = token.some filter (DbGame.defaultToken !=), p = players map (_.encode), s = status.id, t = turns, @@ -366,6 +368,8 @@ object DbGame { val gameIdSize = 8 val playerIdSize = 4 val fullIdSize = 12 + val tokenSize = 4 + val defaultToken = "-tk-" def abandonedDate = DateTime.now - 10.days @@ -380,6 +384,7 @@ object DbGame { mode: Mode, variant: Variant): DbGame = DbGame( id = IdGenerator.game, + token = IdGenerator.token, whitePlayer = whitePlayer withEncodedPieces game.allPieces, blackPlayer = blackPlayer withEncodedPieces game.allPieces, status = Status.Created, @@ -398,6 +403,7 @@ object DbGame { case class RawDbGame( @Key("_id") id: String, + tk: Option[String] = None, p: List[RawDbPlayer], s: Int, t: Int, @@ -423,6 +429,7 @@ case class RawDbGame( trueStatus ← Status(s) } yield DbGame( id = id, + token = tk | DbGame.defaultToken, whitePlayer = whitePlayer, blackPlayer = blackPlayer, status = trueStatus, diff --git a/app/game/GameEnv.scala b/app/game/GameEnv.scala index 416fc8c133..e93219e25b 100644 --- a/app/game/GameEnv.scala +++ b/app/game/GameEnv.scala @@ -33,4 +33,6 @@ final class GameEnv( lazy val listMenu = ListMenu(cached) _ lazy val rewind = new Rewind + + lazy val gameJs = new GameJs(settings.GameJsPath) } diff --git a/app/game/GameJs.scala b/app/game/GameJs.scala new file mode 100644 index 0000000000..03a5bbda1e --- /dev/null +++ b/app/game/GameJs.scala @@ -0,0 +1,14 @@ +package lila +package game + +final class GameJs(path: String) { + + lazy val unsigned: String = { + val source = scala.io.Source fromFile path + source.mkString ~ { _ => source.close } + } + + val placeholder = "--tkph--" + + def sign(token: String) = unsigned.replace(placeholder, token) +} diff --git a/app/game/GameRepo.scala b/app/game/GameRepo.scala index be92bc1d80..d2a7fd3998 100644 --- a/app/game/GameRepo.scala +++ b/app/game/GameRepo.scala @@ -48,6 +48,10 @@ final class GameRepo(collection: MongoCollection) def pov(ref: PovRef): IO[Option[Pov]] = pov(ref.gameId, ref.color) + def token(id: String): IO[String] = io { + primitiveProjection[String](idSelector(id), "tk") | DbGame.defaultToken + } + def save(game: DbGame): IO[Unit] = io { update(idSelector(game), _grater asDBObject game.encode) } diff --git a/app/game/IdGenerator.scala b/app/game/IdGenerator.scala index b046c472b3..b59d0f863a 100644 --- a/app/game/IdGenerator.scala +++ b/app/game/IdGenerator.scala @@ -7,5 +7,7 @@ object IdGenerator { def game = Random nextString DbGame.gameIdSize + def token = Random nextString DbGame.tokenSize + def player = Random nextString DbGame.playerIdSize } diff --git a/app/round/Socket.scala b/app/round/Socket.scala index 0a26b69449..e6f9881ac6 100644 --- a/app/round/Socket.scala +++ b/app/round/Socket.scala @@ -112,15 +112,17 @@ final class Socket( colorName: String, version: Option[Int], uid: Option[String], + token: Option[String], ctx: Context): IO[SocketPromise] = - getWatcherPov(gameId, colorName) map { join(_, false, version, uid, ctx) } + getWatcherPov(gameId, colorName) map { join(_, false, version, uid, token, ctx) } def joinPlayer( fullId: String, version: Option[Int], uid: Option[String], + token: Option[String], ctx: Context): IO[SocketPromise] = - getPlayerPov(fullId) map { join(_, true, version, uid, ctx) } + getPlayerPov(fullId) map { join(_, true, version, uid, token, ctx) } private def parseMove(event: JsValue) = for { d ← event obj "d" @@ -136,9 +138,10 @@ final class Socket( owner: Boolean, versionOption: Option[Int], uidOption: Option[String], + tokenOption: Option[String], ctx: Context): SocketPromise = - ((povOption |@| uidOption |@| versionOption) apply { - (pov: Pov, uid: String, version: Int) ⇒ + ((povOption |@| uidOption |@| tokenOption |@| versionOption) apply { + (pov: Pov, uid: String, token: String, version: Int) ⇒ (for { hub ← hubMaster ? GetHub(pov.gameId) mapTo manifest[ActorRef] socket ← hub ? Join( @@ -146,10 +149,12 @@ final class Socket( user = ctx.me, version = version, color = pov.color, - owner = owner + owner = owner && token == pov.game.token ) map { case Connected(enumerator, member) ⇒ { - if (owner && !member.owner) println("Websocket hijacking detected (%s) %s".format(pov.gameId, ctx.toString)) + if (owner && !member.owner) { + println("Websocket hijacking detected %s %s".format(pov.gameId, ctx.toString)) + } (Iteratee.foreach[JsValue]( controller(hub, uid, member, PovRef(pov.gameId, member.color)) ) mapDone { _ ⇒ diff --git a/app/templating/AssetHelper.scala b/app/templating/AssetHelper.scala index 14e7804887..6df352514f 100644 --- a/app/templating/AssetHelper.scala +++ b/app/templating/AssetHelper.scala @@ -7,7 +7,7 @@ import play.api.templates.Html trait AssetHelper { - val assetVersion = 4 + val assetVersion = 6 def cssTag(name: String) = css("stylesheets/" + name) @@ -20,10 +20,13 @@ trait AssetHelper { def jsTag(name: String) = js("javascripts/" + name) + def jsTagC(name: String) = js("compiled/" + name) + def jsVendorTag(name: String) = js("vendor/" + name) - def js(path: String) = Html { - """""" - .format(routes.Assets.at(path), assetVersion) + private def js(path: String) = jsAt(routes.Assets.at(path).toString) + + def jsAt(path: String) = Html { + """""".format(path, assetVersion) } } diff --git a/app/views/base/layout.scala.html b/app/views/base/layout.scala.html index a97e60edfa..8de5e3a45f 100644 --- a/app/views/base/layout.scala.html +++ b/app/views/base/layout.scala.html @@ -1,4 +1,4 @@ -@(title: String, active: Option[ui.SiteMenu.Elem] = None, baseline: Option[Html] = None, goodies: Option[Html] = None, menu: Option[Html] = None, chat: Option[Html] = None, underchat: Option[Html] = None, robots: Boolean = true, moreCss: Html = Html(""), moreJs: Html = Html(""))(body: Html)(implicit ctx: Context) +@(title: String, active: Option[ui.SiteMenu.Elem] = None, baseline: Option[Html] = None, goodies: Option[Html] = None, menu: Option[Html] = None, chat: Option[Html] = None, underchat: Option[Html] = None, robots: Boolean = true, moreCss: Html = Html(""), moreJs: Html = Html(""), signedJs: Option[String] = None)(body: Html)(implicit ctx: Context) @@ -105,7 +105,7 @@ @jsTag("deps.min.js") - @jsTag("big.js") + @signedJs.fold(jsAt, jsTagC("big.js")) @moreJs @if(lang.language != "en") { diff --git a/app/views/round/layout.scala.html b/app/views/round/layout.scala.html index a1c55fa1b2..08f98d9745 100644 --- a/app/views/round/layout.scala.html +++ b/app/views/round/layout.scala.html @@ -1,8 +1,10 @@ -@(title: String, goodies: Html, chat: Option[Html] = None, underchat: Option[Html] = None, robots: Boolean = true)(body: Html)(implicit ctx: Context) +@(title: String, goodies: Html, chat: Option[Html] = None, underchat: Option[Html] = None, robots: Boolean = true, signedJs: Option[String] = None)(body: Html)(implicit ctx: Context) + @base.layout( title = title, goodies = goodies.some, active = siteMenu.play.some, chat = chat, underchat = underchat, -robots = robots)(body) +robots = robots, +signedJs = signedJs)(body) diff --git a/app/views/round/player.scala.html b/app/views/round/player.scala.html index f7432626f2..14a5e6937d 100644 --- a/app/views/round/player.scala.html +++ b/app/views/round/player.scala.html @@ -14,7 +14,8 @@ title = title, goodies = views.html.game.infoBox(pov, tour), chat = roomHtml.map(round.room(_, false)), -underchat = underchat.some) { +underchat = underchat.some, +signedJs = routes.Round.signedJs(pov.gameId).toString.some) {
controllers.Round.watcher(g GET /$gameId<[\w\-]{8}>/$color controllers.Round.watcher(gameId: String, color: String) GET /$fullId<[\w\-]{12}> controllers.Round.player(fullId: String) GET /$gameId<[\w\-]{8}>/$color/socket controllers.Round.websocketWatcher(gameId: String, color: String) +GET /$gameId<[\w\-]{8}>/signed.js controllers.Round.signedJs(gameId: String) GET /$fullId<[\w\-]{12}>/socket controllers.Round.websocketPlayer(fullId: String) GET /$fullId<[\w\-]{12}>/abort controllers.Round.abort(fullId: String) GET /$fullId<[\w\-]{12}>/resign controllers.Round.resign(fullId: String) diff --git a/public/javascripts/big.js b/public/javascripts/big.js index d6228b3264..6b10ba6fd7 100644 --- a/public/javascripts/big.js +++ b/public/javascripts/big.js @@ -1,3 +1,7 @@ +// ==ClosureCompiler== +// @compilation_level ADVANCED_OPTIMIZATIONS +// @externs_url http://closure-compiler.googlecode.com/svn/trunk/contrib/externs/jquery-1.7.js +// ==/ClosureCompiler== if (typeof console == "undefined" || typeof console.log == "undefined") console = { log: function() {} }; @@ -512,7 +516,7 @@ $.widget("lichess.game", { self.options.tableUrl = self.element.data('table-url'); self.options.playersUrl = self.element.data('players-url'); self.options.socketUrl = self.element.data('socket-url'); - self.socketAckTimeout; + self.socketAckTimeout = null; $("div.game_tournament .clock").each(function() { $(this).clock({time: $(this).data("time")}).clock("start"); @@ -567,6 +571,7 @@ $.widget("lichess.game", { options: { name: "game" }, + params: { tk: "--tkph--"}, events: { ack: function() { clearTimeout(self.socketAckTimeout); @@ -871,12 +876,12 @@ $.widget("lichess.game", { moveData.promotion = "queen"; sendMoveRequest(moveData); } else { - var $choices = $('
').appendTo(self.$board).html('\ -
\ -
\ -
\ -
').fadeIn(self.options.animation_delay).find('div.lichess_piece').click(function() { - moveData.promotion = $(this).attr('data-piece'); + var $choices = $('
') + .appendTo(self.$board) + .html('
') + .fadeIn(self.options.animation_delay) + .find('div.lichess_piece') + .click(function() { moveData.promotion = $(this).attr('data-piece'); sendMoveRequest(moveData); $choices.fadeOut(self.options.animation_delay, function() { $choices.remove(); @@ -1169,7 +1174,7 @@ $.widget("lichess.chat", { }, append: function(msg) { var self = this; - self.$msgs.append(urlToLink(msg))[0]; + self.$msgs.append(urlToLink(msg)); $('body').trigger('lichess.content_loaded'); self.$msgs[0].scrollTop = 9999999; }