diff --git a/play-scala-chatroom-example/.gitignore b/play-scala-chatroom-example/.gitignore
new file mode 100644
index 000000000..193873de8
--- /dev/null
+++ b/play-scala-chatroom-example/.gitignore
@@ -0,0 +1,10 @@
+build
+logs
+target
+/.idea
+/.idea_modules
+/.classpath
+/.gradle
+/.project
+/.settings
+/RUNNING_PID
diff --git a/play-scala-chatroom-example/.mergify.yml b/play-scala-chatroom-example/.mergify.yml
new file mode 100644
index 000000000..32f8689ae
--- /dev/null
+++ b/play-scala-chatroom-example/.mergify.yml
@@ -0,0 +1,27 @@
+pull_request_rules:
+ - name: automatic merge on CI success require review
+ conditions:
+ - status-success=Travis CI - Pull Request
+ - "#approved-reviews-by>=1"
+ - "#changes-requested-reviews-by=0"
+ - label!=block-merge
+ actions:
+ merge:
+ method: squash
+ strict: smart
+
+ - name: automatic merge on CI success for TemplateControl
+ conditions:
+ - status-success=Travis CI - Pull Request
+ - label=merge-when-green
+ - label!=block-merge
+ actions:
+ merge:
+ method: squash
+ strict: smart
+
+ - name: delete branch after merge
+ conditions:
+ - merged
+ actions:
+ delete_head_branch: {}
diff --git a/play-scala-chatroom-example/.travis.yml b/play-scala-chatroom-example/.travis.yml
new file mode 100644
index 000000000..1e8c0e7c4
--- /dev/null
+++ b/play-scala-chatroom-example/.travis.yml
@@ -0,0 +1,50 @@
+language: scala
+scala:
+ - 2.12.8
+
+before_install:
+ - curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | bash && . ~/.jabba/jabba.sh
+
+env:
+ global:
+ - JABBA_HOME=$HOME/.jabba
+ matrix:
+ # There is no concise way to specify multi-dimensional build matrix:
+ # https://github.com/travis-ci/travis-ci/issues/1519
+ - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.8.192-12
+ - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-1
+ - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.192-12
+ - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-1
+
+# Exclude some combinations from build matrix. See:
+# https://docs.travis-ci.com/user/customizing-the-build/#Build-Matrix
+matrix:
+ fast_finish: true
+ allow_failures:
+ # Current release of Gradle still does not supports Play 2.7.x releases
+ # As soon as there is a release of Gradle that fixes that, we can then
+ # remove this allowed failure.
+ - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.192-12
+ - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-1
+ # Java 11 is still not fully supported. It is good that we are already
+ # testing our sample applications to better discover possible problems
+ # but we can allow failures here too.
+ - env: SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-1
+
+install:
+ - $JABBA_HOME/bin/jabba install $TRAVIS_JDK
+ - unset _JAVA_OPTIONS
+ - export JAVA_HOME="$JABBA_HOME/jdk/$TRAVIS_JDK" && export PATH="$JAVA_HOME/bin:$PATH" && java -Xmx32m -version
+
+script:
+ - $SCRIPT
+
+before_cache:
+ - find $HOME/.ivy2 -name "ivydata-*.properties" -delete
+ - find $HOME/.sbt -name "*.lock" -delete
+
+cache:
+ directories:
+ - "$HOME/.ivy2/cache"
+ - "$HOME/.gradle/caches"
+ - "$HOME/.jabba/jdk"
diff --git a/play-scala-chatroom-example/LICENSE b/play-scala-chatroom-example/LICENSE
new file mode 100644
index 000000000..670154e35
--- /dev/null
+++ b/play-scala-chatroom-example/LICENSE
@@ -0,0 +1,116 @@
+CC0 1.0 Universal
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific
+works ("Commons") that the public can reliably and without fear of later
+claims of infringement build upon, modify, incorporate in other works, reuse
+and redistribute as freely as possible in any form whatsoever and for any
+purposes, including without limitation commercial purposes. These owners may
+contribute to the Commons to promote the ideal of a free culture and the
+further production of creative, cultural and scientific works, or to gain
+reputation or greater distribution for their Work in part through the use and
+efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation
+of additional consideration or compensation, the person associating CC0 with a
+Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
+and publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not limited
+to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display, communicate,
+ and translate a Work;
+
+ ii. moral rights retained by the original author(s) and/or performer(s);
+
+ iii. publicity and privacy rights pertaining to a person's image or likeness
+ depicted in a Work;
+
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+
+ v. rights protecting the extraction, dissemination, use and reuse of data in
+ a Work;
+
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation thereof,
+ including any amended or successor version of such directive); and
+
+ vii. other similar, equivalent or corresponding rights throughout the world
+ based on applicable law or treaty, and any national implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
+and Related Rights and associated claims and causes of action, whether now
+known or unknown (including existing as well as future claims and causes of
+action), in the Work (i) in all territories worldwide, (ii) for the maximum
+duration provided by applicable law or treaty (including future time
+extensions), (iii) in any current or future medium and for any number of
+copies, and (iv) for any purpose whatsoever, including without limitation
+commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
+the Waiver for the benefit of each member of the public at large and to the
+detriment of Affirmer's heirs and successors, fully intending that such Waiver
+shall not be subject to revocation, rescission, cancellation, termination, or
+any other legal or equitable action to disrupt the quiet enjoyment of the Work
+by the public as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+judged legally invalid or ineffective under applicable law, then the Waiver
+shall be preserved to the maximum extent permitted taking into account
+Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
+is so judged Affirmer hereby grants to each affected person a royalty-free,
+non transferable, non sublicensable, non exclusive, irrevocable and
+unconditional license to exercise Affirmer's Copyright and Related Rights in
+the Work (i) in all territories worldwide, (ii) for the maximum duration
+provided by applicable law or treaty (including future time extensions), (iii)
+in any current or future medium and for any number of copies, and (iv) for any
+purpose whatsoever, including without limitation commercial, advertising or
+promotional purposes (the "License"). The License shall be deemed effective as
+of the date CC0 was applied by Affirmer to the Work. Should any part of the
+License for any reason be judged legally invalid or ineffective under
+applicable law, such partial invalidity or ineffectiveness shall not
+invalidate the remainder of the License, and in such case Affirmer hereby
+affirms that he or she will not (i) exercise any of his or her remaining
+Copyright and Related Rights in the Work or (ii) assert any associated claims
+and causes of action with respect to the Work, in either case contrary to
+Affirmer's express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+
+ b. Affirmer offers the Work as-is and makes no representations or warranties
+ of any kind concerning the Work, express, implied, statutory or otherwise,
+ including without limitation warranties of title, merchantability, fitness
+ for a particular purpose, non infringement, or the absence of latent or
+ other defects, accuracy, or the present or absence of errors, whether or not
+ discoverable, all to the greatest extent permissible under applicable law.
+
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without limitation
+ any person's Copyright and Related Rights in the Work. Further, Affirmer
+ disclaims responsibility for obtaining any necessary consents, permissions
+ or other rights required for any use of the Work.
+
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to this
+ CC0 or use of the Work.
+
+For more information, please see
+
diff --git a/play-scala-chatroom-example/NOTICE b/play-scala-chatroom-example/NOTICE
new file mode 100644
index 000000000..6d6c034d3
--- /dev/null
+++ b/play-scala-chatroom-example/NOTICE
@@ -0,0 +1,8 @@
+Written by Lightbend
+
+To the extent possible under law, the author(s) have dedicated all copyright and
+related and neighboring rights to this software to the public domain worldwide.
+This software is distributed without any warranty.
+
+You should have received a copy of the CC0 Public Domain Dedication along with
+this software. If not, see .
diff --git a/play-scala-chatroom-example/README.md b/play-scala-chatroom-example/README.md
new file mode 100644
index 000000000..cfcd14f76
--- /dev/null
+++ b/play-scala-chatroom-example/README.md
@@ -0,0 +1,74 @@
+# play-scala-chatroom-example
+
+[](https://travis-ci.org/playframework/play-scala-chatroom-example)
+
+This is a simple chatroom using Play and Websockets with the Scala API.
+
+This project makes use of [dynamic streams](http://doc.akka.io/docs/akka/current/scala/stream/stream-dynamic.html) from Akka Streams, notably `BroadcastHub` and `MergeHub`. By [combining MergeHub and BroadcastHub](http://doc.akka.io/docs/akka/current/scala/stream/stream-dynamic.html#Dynamic_fan-in_and_fan-out_with_MergeHub_and_BroadcastHub), you can get publish/subscribe functionality.
+
+## The good bit
+
+The flow is defined once in the controller, and used everywhere from the `chat` action:
+
+```scala
+class HomeController extends Controller {
+
+ // chat room many clients -> merge hub -> broadcasthub -> many clients
+ private val (chatSink, chatSource) = {
+
+ // Don't log MergeHub$ProducerFailed as error if the client disconnects.
+ // recoverWithRetries -1 is essentially "recoverWith"
+ val source = MergeHub.source[WSMessage]
+ .log("source")
+ .recoverWithRetries(-1, { case _: Exception ⇒ Source.empty })
+
+ val sink = BroadcastHub.sink[WSMessage]
+ source.toMat(sink)(Keep.both).run()
+ }
+
+ private val userFlow: Flow[WSMessage, WSMessage, _] = {
+ Flow[WSMessage].via(Flow.fromSinkAndSource(chatSink, chatSource)).log("userFlow")
+ }
+
+ def chat: WebSocket = {
+ WebSocket.acceptOrResult[WSMessage, WSMessage] {
+ case rh if sameOriginCheck(rh) =>
+ Future.successful(userFlow).map { flow =>
+ Right(flow)
+ }.recover {
+ case e: Exception =>
+ val msg = "Cannot create websocket"
+ logger.error(msg, e)
+ val result = InternalServerError(msg)
+ Left(result)
+ }
+
+ case rejected =>
+ logger.error(s"Request ${rejected} failed same origin check")
+ Future.successful {
+ Left(Forbidden("forbidden"))
+ }
+ }
+ }
+}
+```
+
+## Prerequisites
+
+You will need [JDK 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) and [sbt](http://www.scala-sbt.org/) installed.
+
+## Running
+
+```bash
+sbt run
+```
+
+Go to and open it in two different browsers. Typing into one browser will cause it to show up in another browser.
+
+## Tributes
+
+This project is originally taken from Johan Andrén's [Akka-HTTP version](https://github.com/johanandren/chat-with-akka-http-websockets/tree/akka-2.4.10):
+
+Johan also has a blog post explaining dynamic streams in more detail:
+
+*
diff --git a/play-scala-chatroom-example/app/controllers/HomeController.scala b/play-scala-chatroom-example/app/controllers/HomeController.scala
new file mode 100644
index 000000000..13acb6fae
--- /dev/null
+++ b/play-scala-chatroom-example/app/controllers/HomeController.scala
@@ -0,0 +1,115 @@
+package controllers
+
+import java.net.URI
+import javax.inject._
+
+import akka.actor.ActorSystem
+import akka.event.Logging
+import akka.stream.Materializer
+import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, MergeHub, Source}
+import play.api.{Logger, MarkerContext}
+import play.api.mvc._
+
+import scala.concurrent.{ExecutionContext, Future}
+
+/**
+ * A very simple chat client using websockets.
+ */
+@Singleton
+class HomeController @Inject()(cc: ControllerComponents)
+ (implicit actorSystem: ActorSystem,
+ mat: Materializer,
+ executionContext: ExecutionContext,
+ webJarsUtil: org.webjars.play.WebJarsUtil)
+ extends AbstractController(cc) with RequestMarkerContext {
+
+ private type WSMessage = String
+
+ private val logger = Logger(getClass)
+
+ private implicit val logging = Logging(actorSystem.eventStream, logger.underlyingLogger.getName)
+
+ // chat room many clients -> merge hub -> broadcasthub -> many clients
+ private val (chatSink, chatSource) = {
+ // Don't log MergeHub$ProducerFailed as error if the client disconnects.
+ // recoverWithRetries -1 is essentially "recoverWith"
+ val source = MergeHub.source[WSMessage]
+ .log("source")
+ .recoverWithRetries(-1, { case _: Exception ⇒ Source.empty })
+
+ val sink = BroadcastHub.sink[WSMessage]
+ source.toMat(sink)(Keep.both).run()
+ }
+
+ private val userFlow: Flow[WSMessage, WSMessage, _] = {
+ Flow.fromSinkAndSource(chatSink, chatSource)
+ }
+
+ def index: Action[AnyContent] = Action { implicit request: RequestHeader =>
+ val webSocketUrl = routes.HomeController.chat().webSocketURL()
+ logger.info(s"index: ")
+ Ok(views.html.index(webSocketUrl))
+ }
+
+ def chat(): WebSocket = {
+ WebSocket.acceptOrResult[WSMessage, WSMessage] {
+ case rh if sameOriginCheck(rh) =>
+ Future.successful(userFlow).map { flow =>
+ Right(flow)
+ }.recover {
+ case e: Exception =>
+ val msg = "Cannot create websocket"
+ logger.error(msg, e)
+ val result = InternalServerError(msg)
+ Left(result)
+ }
+
+ case rejected =>
+ logger.error(s"Request ${rejected} failed same origin check")
+ Future.successful {
+ Left(Forbidden("forbidden"))
+ }
+ }
+ }
+
+ /**
+ * Checks that the WebSocket comes from the same origin. This is necessary to protect
+ * against Cross-Site WebSocket Hijacking as WebSocket does not implement Same Origin Policy.
+ *
+ * See https://tools.ietf.org/html/rfc6455#section-1.3 and
+ * http://blog.dewhurstsecurity.com/2013/08/30/security-testing-html5-websockets.html
+ */
+ private def sameOriginCheck(implicit rh: RequestHeader): Boolean = {
+ // The Origin header is the domain the request originates from.
+ // https://tools.ietf.org/html/rfc6454#section-7
+ logger.debug("Checking the ORIGIN ")
+
+ rh.headers.get("Origin") match {
+ case Some(originValue) if originMatches(originValue) =>
+ logger.debug(s"originCheck: originValue = $originValue")
+ true
+
+ case Some(badOrigin) =>
+ logger.error(s"originCheck: rejecting request because Origin header value ${badOrigin} is not in the same origin")
+ false
+
+ case None =>
+ logger.error("originCheck: rejecting request because no Origin header found")
+ false
+ }
+ }
+
+ /**
+ * Returns true if the value of the Origin header contains an acceptable value.
+ */
+ private def originMatches(origin: String): Boolean = {
+ try {
+ val url = new URI(origin)
+ url.getHost == "localhost" &&
+ (url.getPort match { case 9000 | 19001 => true; case _ => false })
+ } catch {
+ case e: Exception => false
+ }
+ }
+
+}
diff --git a/play-scala-chatroom-example/app/controllers/RequestMarkerContext.scala b/play-scala-chatroom-example/app/controllers/RequestMarkerContext.scala
new file mode 100644
index 000000000..53c6fdae1
--- /dev/null
+++ b/play-scala-chatroom-example/app/controllers/RequestMarkerContext.scala
@@ -0,0 +1,23 @@
+package controllers
+
+import play.api.MarkerContext
+import play.api.mvc._
+
+import scala.language.implicitConversions
+
+/**
+ * Provide host and path logging on the request, available in application.json
+ */
+trait RequestMarkerContext {
+
+ implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
+ import net.logstash.logback.marker.LogstashMarker
+ import net.logstash.logback.marker.Markers._
+
+ val requestMarkers: LogstashMarker = append("host", request.host)
+ .and(append("path", request.path))
+
+ MarkerContext(requestMarkers)
+ }
+
+}
\ No newline at end of file
diff --git a/play-scala-chatroom-example/app/filters/ContentSecurityPolicyFilter.scala b/play-scala-chatroom-example/app/filters/ContentSecurityPolicyFilter.scala
new file mode 100644
index 000000000..46c3b34d1
--- /dev/null
+++ b/play-scala-chatroom-example/app/filters/ContentSecurityPolicyFilter.scala
@@ -0,0 +1,22 @@
+package filters
+
+import javax.inject.Inject
+
+import controllers.routes
+import play.api.mvc.{EssentialAction, EssentialFilter, RequestHeader}
+
+import scala.concurrent.ExecutionContext
+
+/**
+ * Set up a more flexible content security policy that points to self and the given
+ * websocket URL.
+ */
+class ContentSecurityPolicyFilter @Inject()(implicit ec: ExecutionContext) extends EssentialFilter {
+
+ override def apply(next: EssentialAction): EssentialAction = EssentialAction { request: RequestHeader =>
+ val webSocketUrl = routes.HomeController.chat().webSocketURL()(request)
+ next(request).map { result =>
+ result.withHeaders("Content-Security-Policy" -> s"connect-src 'self' $webSocketUrl")
+ }
+ }
+}
diff --git a/play-scala-chatroom-example/app/views/index.scala.html b/play-scala-chatroom-example/app/views/index.scala.html
new file mode 100644
index 000000000..41b16e4c3
--- /dev/null
+++ b/play-scala-chatroom-example/app/views/index.scala.html
@@ -0,0 +1,56 @@
+@(webSocketUrl: String)(implicit webJarsUtil: org.webjars.play.WebJarsUtil)
+
+
+
+
+
+
+
+ @Html(webJarsUtil.css("bootstrap.min.css"))
+ @Html(webJarsUtil.css("bootstrap-theme.min.css"))
+
+
+
+ Chat Room
+
+
+
+
+
+
+
+
Chat Room
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @Html(webJarsUtil.script("jquery.min.js"))
+ @Html(webJarsUtil.script("jquery.flot.min.js"))
+
+
+
+
+
+
\ No newline at end of file
diff --git a/play-scala-chatroom-example/build.gradle b/play-scala-chatroom-example/build.gradle
new file mode 100644
index 000000000..b9f73c748
--- /dev/null
+++ b/play-scala-chatroom-example/build.gradle
@@ -0,0 +1,53 @@
+plugins {
+ id 'play'
+ id 'idea'
+}
+
+def playVersion = "2.6.21"
+def akkaVersion = '2.5.8'
+def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12")
+
+model {
+ components {
+ play {
+ platform play: playVersion, scala: scalaVersion, java: '1.8'
+ injectedRoutesGenerator = true
+
+ sources {
+ twirlTemplates {
+ defaultImports = TwirlImports.SCALA
+ }
+ }
+ }
+ }
+}
+
+dependencies {
+ play "com.typesafe.play:play-guice_$scalaVersion:$playVersion"
+ play "com.typesafe.play:play-logback_$scalaVersion:$playVersion"
+
+ play "org.webjars:webjars-play_$scalaVersion:2.6.2
+ play "org.webjars:flot:0.8.3"
+ play "org.webjars:bootstrap:3.3.6"
+ play "net.logstash.logback:logstash-logback-encoder:4.11"
+
+ play "com.typesafe.akka:akka-slf4j_$scalaVersion:$akkaVersion"
+ play "ch.qos.logback:logback-classic:1.2.3"
+
+ playTest "org.scalatestplus.play:scalatestplus-play_$scalaVersion:3.1.2"
+ playTest "com.typesafe.akka:akka-testkit_$scalaVersion:$akkaVersion"
+ playTest "com.typesafe.akka:akka-stream-testkit_$scalaVersion:$akkaVersion"
+}
+
+repositories {
+ jcenter()
+ maven {
+ name "lightbend-maven-releases"
+ url "https://repo.lightbend.com/lightbend/maven-release"
+ }
+ ivy {
+ name "lightbend-ivy-release"
+ url "https://repo.lightbend.com/lightbend/ivy-releases"
+ layout "ivy"
+ }
+}
diff --git a/play-scala-chatroom-example/build.sbt b/play-scala-chatroom-example/build.sbt
new file mode 100644
index 000000000..1831d0275
--- /dev/null
+++ b/play-scala-chatroom-example/build.sbt
@@ -0,0 +1,27 @@
+val akkaVersion = "2.5.8"
+
+lazy val root = (project in file(".")).enablePlugins(PlayScala)
+
+name := """play-chatroom-scala-example"""
+
+version := "2.6.x"
+
+scalaVersion := "2.12.8"
+
+crossScalaVersions := Seq("2.11.12", "2.12.4")
+
+libraryDependencies += guice
+
+libraryDependencies += "org.webjars" %% "webjars-play" % "2.6.2"
+libraryDependencies += "org.webjars" % "flot" % "0.8.3"
+libraryDependencies += "org.webjars" % "bootstrap" % "3.3.6"
+
+// https://mvnrepository.com/artifact/net.logstash.logback/logstash-logback-encoder
+libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "4.11"
+
+libraryDependencies += "com.typesafe.akka" %% "akka-slf4j" % akkaVersion
+libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
+
+libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test
+libraryDependencies += "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test
+libraryDependencies += "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test
diff --git a/play-scala-chatroom-example/conf/application.conf b/play-scala-chatroom-example/conf/application.conf
new file mode 100644
index 000000000..9956ddbd7
--- /dev/null
+++ b/play-scala-chatroom-example/conf/application.conf
@@ -0,0 +1,16 @@
+// Enable richer akka logging
+akka {
+ loggers = ["akka.event.slf4j.Slf4jLogger"]
+ loglevel = "DEBUG"
+ logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
+}
+
+// https://www.playframework.com/documentation/2.6.x/SecurityHeaders
+// Disable the out of the box content security policy in SecurityHeadersFilter
+play.filters.headers.contentSecurityPolicy = null
+
+// https://www.playframework.com/documentation/2.6.x/AllowedHostsFilter
+play.filters.hosts.allowed = ["localhost:9000", "localhost:19001"]
+
+// Add CSP header in explicitly in a custom filter.
+play.filters.enabled += filters.ContentSecurityPolicyFilter
diff --git a/play-scala-chatroom-example/conf/logback.xml b/play-scala-chatroom-example/conf/logback.xml
new file mode 100644
index 000000000..b8516819c
--- /dev/null
+++ b/play-scala-chatroom-example/conf/logback.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ ${application.home:-.}/logs/application.log
+
+ %date [%level] from %logger in %thread - %message%n%xException
+
+
+
+
+ ${application.home:-.}/logs/application.json
+
+
+
+
+
+
+
+
+
+
+
+ %coloredLevel %logger{15} - %message%n%xException{10}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/play-scala-chatroom-example/conf/messages b/play-scala-chatroom-example/conf/messages
new file mode 100644
index 000000000..e69de29bb
diff --git a/play-scala-chatroom-example/conf/routes b/play-scala-chatroom-example/conf/routes
new file mode 100644
index 000000000..f1a326a22
--- /dev/null
+++ b/play-scala-chatroom-example/conf/routes
@@ -0,0 +1,12 @@
+# Routes
+# This file defines all application routes (Higher priority routes first)
+# ~~~~
+
+# An example controller showing a sample home page
+GET / controllers.HomeController.index
+GET /chat controllers.HomeController.chat
+
+# Map static resources from the /public folder to the /assets URL path
+GET /assets/*file controllers.Assets.at(path="/public", file)
+
+-> /webjars webjars.Routes
diff --git a/play-scala-chatroom-example/gradle/wrapper/gradle-wrapper.jar b/play-scala-chatroom-example/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..01b8bf6b1
Binary files /dev/null and b/play-scala-chatroom-example/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/play-scala-chatroom-example/gradle/wrapper/gradle-wrapper.properties b/play-scala-chatroom-example/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..89dba2d9d
--- /dev/null
+++ b/play-scala-chatroom-example/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/play-scala-chatroom-example/gradlew b/play-scala-chatroom-example/gradlew
new file mode 100755
index 000000000..cccdd3d51
--- /dev/null
+++ b/play-scala-chatroom-example/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/play-scala-chatroom-example/gradlew.bat b/play-scala-chatroom-example/gradlew.bat
new file mode 100644
index 000000000..e95643d6a
--- /dev/null
+++ b/play-scala-chatroom-example/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/play-scala-chatroom-example/project/build.properties b/play-scala-chatroom-example/project/build.properties
new file mode 100644
index 000000000..c0bab0494
--- /dev/null
+++ b/play-scala-chatroom-example/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.2.8
diff --git a/play-scala-chatroom-example/project/plugins.sbt b/play-scala-chatroom-example/project/plugins.sbt
new file mode 100644
index 000000000..b21346c41
--- /dev/null
+++ b/play-scala-chatroom-example/project/plugins.sbt
@@ -0,0 +1 @@
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.21")
diff --git a/play-scala-chatroom-example/public/images/favicon.png b/play-scala-chatroom-example/public/images/favicon.png
new file mode 100644
index 000000000..c7d92d2ae
Binary files /dev/null and b/play-scala-chatroom-example/public/images/favicon.png differ
diff --git a/play-scala-chatroom-example/public/javascripts/app.js b/play-scala-chatroom-example/public/javascripts/app.js
new file mode 100644
index 000000000..0127f10a2
--- /dev/null
+++ b/play-scala-chatroom-example/public/javascripts/app.js
@@ -0,0 +1,47 @@
+$( document ).ready(function() {
+ if ("WebSocket" in window) {
+ console.log("WebSocket is supported by your Browser!");
+ } else {
+ console.log("WebSocket NOT supported by your Browser!");
+ return;
+ }
+ var getScriptParamUrl = function() {
+ var scripts = document.getElementsByTagName('script');
+ var lastScript = scripts[scripts.length-1];
+ return lastScript.getAttribute('data-url');
+ };
+
+ var send = function() {
+ var text = $message.val();
+ $message.val("");
+ connection.send(text);
+ };
+
+ var $messages = $("#messages"), $send = $("#send"), $message = $("#message");
+
+ var url = getScriptParamUrl();
+ var connection = new WebSocket(url);
+
+ $send.prop("disabled", true);
+
+ connection.onopen = function() {
+ $send.prop("disabled", false);
+ $messages
+ .prepend($("