204 lines
7.9 KiB
Scala
204 lines
7.9 KiB
Scala
package controllers
|
|
|
|
import javax.inject._
|
|
import actors._
|
|
import akka.NotUsed
|
|
import akka.actor._
|
|
import akka.event.Logging
|
|
import akka.pattern.ask
|
|
import akka.stream._
|
|
import akka.stream.scaladsl._
|
|
import akka.util.Timeout
|
|
import com.typesafe.config.ConfigFactory
|
|
import org.reactivestreams.Publisher
|
|
import play.api.libs.json._
|
|
import play.api.mvc._
|
|
|
|
import scala.concurrent.duration._
|
|
import scala.concurrent.{ExecutionContext, Future}
|
|
import scala.util.Try
|
|
|
|
case class LastMapsSelectConfig(last1: Boolean, last3: Boolean, last5: Boolean, last7: Boolean)
|
|
case class RaceSelect(select: Boolean, showAtStart: Boolean)
|
|
|
|
/**
|
|
* This class creates the actions and the websocket needed.
|
|
*/
|
|
@Singleton
|
|
class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
|
|
|
|
implicit val actorSystem: ActorSystem = ActorSystem()
|
|
implicit val ec: ExecutionContext = defaultExecutionContext
|
|
|
|
val config = ConfigFactory.load()
|
|
|
|
// Use a direct reference to SLF4J
|
|
private val logger = org.slf4j.LoggerFactory.getLogger("controllers.HomeController")
|
|
|
|
|
|
val userParentActor: ActorRef = actorSystem.actorOf(Props(classOf[UserParentActor], actorSystem))
|
|
|
|
// Home page that renders template
|
|
def index(deciderName: String) = Action { implicit request =>
|
|
logger.info(s"Received request from: ${request.remoteAddress}")
|
|
val deciderHumanName = config.getString(s"deciders.$deciderName.name")
|
|
val deciderDescription = config.getString(s"deciders.$deciderName.rules")
|
|
val isSolo = Try(config.getBoolean(s"deciders.$deciderName.isSolo")).getOrElse(false)
|
|
val raceCount = Try(config.getInt(s"deciders.$deciderName.raceCount")).getOrElse(1)
|
|
val lastmapsSettings = LastMapsSelectConfig(true, true, true, true)
|
|
val raceSelect = RaceSelect(true, false)
|
|
Ok(views.html.index(deciderName, deciderHumanName, raceCount, deciderDescription, raceSelect, lastmapsSettings, isSolo))
|
|
}
|
|
|
|
/**
|
|
* Creates a websocket. `acceptOrResult` is preferable here because it returns a
|
|
* Future[Flow], which is required internally.
|
|
*
|
|
* @return a fully realized websocket.
|
|
*/
|
|
def ws: WebSocket = WebSocket.acceptOrResult[JsValue, JsValue] {
|
|
case rh if sameOriginCheck(rh) =>
|
|
wsFutureFlow(rh).map { flow =>
|
|
Right(flow)
|
|
}.recover {
|
|
case e: Exception =>
|
|
logger.error("Cannot create websocket", e)
|
|
val jsError = Json.obj("error" -> "Cannot create websocket")
|
|
val result = Results.InternalServerError(jsError)
|
|
Left(result)
|
|
}
|
|
|
|
case rejected =>
|
|
logger.error(s"Request ${rejected} failed same origin check")
|
|
Future.successful {
|
|
Left(Results.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
|
|
*/
|
|
def sameOriginCheck(rh: RequestHeader): Boolean = {
|
|
rh.headers.get("Origin") match {
|
|
case Some(originValue) if originMatches(originValue, rh.remoteAddress) =>
|
|
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.
|
|
*/
|
|
def originMatches(origin: String, remoteAddress: String): Boolean = {
|
|
origin.contains("89.108.83.108") || origin.contains("crosspick.ru") || origin.contains("localhost") || origin.contains("localhost:9000") || origin.contains("localhost:19001")
|
|
}
|
|
|
|
/**
|
|
* Creates a Future containing a Flow of JsValue in and out.
|
|
*/
|
|
private def wsFutureFlow(request: RequestHeader): Future[Flow[JsValue, JsValue, NotUsed]] = {
|
|
// create an actor ref source and associated publisher for sink
|
|
val (webSocketOut: ActorRef, webSocketIn: Publisher[JsValue]) = createWebSocketConnections()
|
|
|
|
// Create a user actor off the request id and attach it to the source
|
|
val userActorFuture = createUserActor(request.id.toString, request.remoteAddress, webSocketOut)
|
|
|
|
// Once we have an actor available, create a flow...
|
|
userActorFuture.map { userActor =>
|
|
createWebSocketFlow(webSocketIn, userActor)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a materialized flow for the websocket, exposing the source and sink.
|
|
*
|
|
* @return the materialized input and output of the flow.
|
|
*/
|
|
def createWebSocketConnections(): (ActorRef, Publisher[JsValue]) = {
|
|
|
|
// Creates a source to be materialized as an actor reference.
|
|
val source: Source[JsValue, ActorRef] = {
|
|
// If you want to log on a flow, you have to use a logging adapter.
|
|
// http://doc.akka.io/docs/akka/2.4.4/scala/logging.html#SLF4J
|
|
val logging = Logging(actorSystem.eventStream, logger.getName)
|
|
|
|
// Creating a source can be done through various means, but here we want
|
|
// the source exposed as an actor so we can send it messages from other
|
|
// actors.
|
|
Source.actorRef[JsValue](10, OverflowStrategy.dropTail).log("actorRefSource")(logging)
|
|
}
|
|
|
|
// Creates a sink to be materialized as a publisher. Fanout is false as we only want
|
|
// a single subscriber here.
|
|
val sink: Sink[JsValue, Publisher[JsValue]] = Sink.asPublisher(fanout = false)
|
|
|
|
// Connect the source and sink into a flow, telling it to keep the materialized values,
|
|
// and then kicks the flow into existence.
|
|
source.toMat(sink)(Keep.both).run()
|
|
}
|
|
|
|
/**
|
|
* Creates a flow of events from the websocket to the user actor.
|
|
*
|
|
* When the flow is terminated, the user actor is no longer needed and is stopped.
|
|
*
|
|
* @param userActor the user actor receiving websocket events.
|
|
* @param webSocketIn the "read" side of the websocket, that publishes JsValue to UserActor.
|
|
* @return a Flow of JsValue in both directions.
|
|
*/
|
|
def createWebSocketFlow(webSocketIn: Publisher[JsValue], userActor: ActorRef): Flow[JsValue, JsValue, NotUsed] = {
|
|
// http://doc.akka.io/docs/akka/current/scala/stream/stream-flows-and-basics.html#stream-materialization
|
|
// http://doc.akka.io/docs/akka/current/scala/stream/stream-integrations.html#integrating-with-actors
|
|
|
|
// source is what comes in: browser ws events -> play -> publisher -> userActor
|
|
// sink is what comes out: userActor -> websocketOut -> play -> browser ws events
|
|
val flow = {
|
|
val sink = Sink.actorRef(userActor, akka.actor.Status.Success(()))
|
|
val source = Source.fromPublisher(webSocketIn)
|
|
Flow.fromSinkAndSource(sink, source)
|
|
}
|
|
|
|
// Unhook the user actor when the websocket flow terminates
|
|
// http://doc.akka.io/docs/akka/current/scala/stream/stages-overview.html#watchTermination
|
|
val flowWatch: Flow[JsValue, JsValue, NotUsed] = flow.watchTermination() { (_, termination) =>
|
|
termination.foreach { done =>
|
|
logger.info(s"Terminating actor $userActor")
|
|
LobbiesActor.actor.tell(UnWatchAllLobbies, userActor)
|
|
actorSystem.stop(userActor)
|
|
}
|
|
NotUsed
|
|
}
|
|
|
|
flowWatch
|
|
}
|
|
|
|
/**
|
|
* Creates a user actor with a given name, using the websocket out actor for output.
|
|
*
|
|
* @param name the name of the user actor.
|
|
* @param webSocketOut the "write" side of the websocket, that the user actor sends JsValue to.
|
|
* @return a user actor for this ws connection.
|
|
*/
|
|
private def createUserActor(name: String, remoteAddress: String, webSocketOut: ActorRef): Future[ActorRef] = {
|
|
// Use guice assisted injection to instantiate and configure the child actor.
|
|
val userActorFuture = {
|
|
implicit val timeout = Timeout(100.millis)
|
|
(userParentActor ? UserParentActor.Create(name, remoteAddress, webSocketOut)).mapTo[ActorRef]
|
|
}
|
|
userActorFuture
|
|
}
|
|
|
|
}
|