All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xitrum.sockjs.SockJsActions.scala Maven / Gradle / Ivy

The newest version!
package xitrum.sockjs

import java.util.Random
import scala.concurrent.duration._
import scala.util.control.NonFatal

import io.netty.buffer.Unpooled
import io.netty.channel.{ChannelFuture, ChannelFutureListener}
import io.netty.handler.codec.http.{HttpHeaderNames, HttpHeaderValues, HttpResponseStatus, HttpUtil}
import io.netty.handler.codec.http.cookie.DefaultCookie

import akka.actor.{ActorRef, Props, ReceiveTimeout, Terminated}
import glokka.Registry

import xitrum.{
  Action, ActorAction, Config, SkipCsrfCheck, SockJsText,
  WebSocketAction, WebSocketText
}
import xitrum.annotation._
import xitrum.scope.request.PathInfo
import xitrum.util.SeriDeseri
import xitrum.view.DocType

private object NotificationToHandlerUtil {
  def onComplete(
    channelFuture: ChannelFuture,
    index: Int, sockJsActorRef: ActorRef, write: Boolean
  ): ChannelFuture = {
    channelFuture.addListener(new ChannelFutureListener {
      def operationComplete(f: ChannelFuture): Unit = {
        val msg =
          if (write) {
            if (f.isSuccess)
              NotificationToHandlerChannelWriteSuccess(index)
            else
              NotificationToHandlerChannelWriteFailure(index)
          } else {
            if (f.isSuccess)
              NotificationToHandlerChannelCloseSuccess(index)
            else
              NotificationToHandlerChannelCloseFailure(index)
          }

        sockJsActorRef ! msg
      }
    })
    channelFuture
  }
}

// General info:
// http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.html
// http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
// https://developer.mozilla.org/en-US/docs/HTTP_access_control
//
// Reference implementation (need to read when in doubt):
// https://github.com/sockjs/sockjs-node/tree/master/src
object SockJsAction {
  // The server must send a heartbeat frame every 25 seconds
  // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-46
  val TIMEOUT_HEARTBEAT: FiniteDuration = 25.seconds

  // All the chunking transports are closed by the server after 128K (production mode)
  // or 4K (development mode) was sent, in order to force client to GC and reconnect.
  // Last chunk is forcefully sent when limit is reached.
  val CHUNKED_RESPONSE_LIMIT: Int = if (Config.productionMode) 128 * 1024 else 4 * 1024

  val actorRegistry: ActorRef = Registry.start(Config.actorSystem, getClass.getName)

  //----------------------------------------------------------------------------

  private[this] val random = new Random(System.currentTimeMillis())

  /** `0 to 2^32 - 1` */
  def entropy(): Int = random.nextInt().abs

  //----------------------------------------------------------------------------

  /** 2K 'h' characters */
  val H2K: Array[Byte] = {
    val bytes   = Array.fill[Byte](2048 + 1)('h')
    bytes(2048) = '\n'
    bytes
  }

  private[this] val HTML_TEMPLATE_BEFORE = """

  
  

Don't panic!

""" // Nearly 1K spaces. // // Safari needs at least 1024 bytes to parse the website: // http://code.google.com/p/browsersec/wiki/Part2#Survey_of_content_sniffing_behaviors // // https://github.com/sockjs/sockjs-node/blob/master/src/trans-htmlfile.coffee#L29 // http://stackoverflow.com/questions/2804827/create-a-string-with-n-characters private[this] val S1K = { val numSpaces = 1024 - HTML_TEMPLATE_BEFORE.length + HTML_TEMPLATE_AFTER.length val bytes = Array.fill[Byte](numSpaces)(' ') new String(bytes) + "\r\n\r\n" } /** Template for htmlfile transport */ def htmlFile(callback: String, with1KSpaces: Boolean): String = { val template = HTML_TEMPLATE_BEFORE + callback + HTML_TEMPLATE_AFTER if (with1KSpaces) template + S1K else template } //---------------------------------------------------------------------------- // https://groups.google.com/group/sockjs/msg/9da24b0dde8916e4 // https://groups.google.com/group/sockjs/msg/b63cd4555bd69ae4 // https://github.com/sockjs/sockjs-node/blob/master/src/utils.coffee#L87-L109 def quoteUnicode(string: String): String = { val b = new StringBuilder string.foreach { c => if (('\u0000' <= c && c <= '\u001f') || ('\ud800' <= c && c <= '\udfff') || ('\u200c' <= c && c <= '\u200f') || ('\u2028' <= c && c <= '\u202f') || ('\u2060' <= c && c <= '\u206f') || ('\ufff0' <= c && c <= '\uffff')) { val hex = Integer.toHexString(c) val len = hex.length b.append("\\u") if (len == 1) b.append("000") else if (len == 2) b.append("00") else if (len == 3) b.append("0") b.append(hex) } else { b.append(c) } } b.toString } } trait ServerIdSessionIdValidator extends Action { // Server ID and session ID can't contain dots // (placeholder in URL can't be empty, no need to check) beforeFilter { if (pathParams.contains("serverId") || pathParams.contains("sessionId")) { val noDots = pathParams("serverId").head.indexOf('.') < 0 && pathParams("sessionId").head.indexOf('.') < 0 if (!noDots) { response.setStatus(HttpResponseStatus.NOT_FOUND) respondText("") } } } } trait SockJsPrefix { var pathPrefix = "" /** Called by Dispatcher. */ def setPathPrefix(pathInfo: PathInfo): Unit = { val n = nLastTokensToRemoveFromPathInfo val decoded = pathInfo.decoded pathPrefix = if (n == 0) decoded.substring(1) else if (n == 1) decoded.substring(1, decoded.lastIndexOf("/")) else { val tokens = pathInfo.tokens tokens.take(tokens.length - n).mkString("/") } } protected def nLastTokensToRemoveFromPathInfo: Int } trait SockJsAction extends ServerIdSessionIdValidator with SockJsPrefix { // JSESSIONID cookie must be echoed back if sent by the client, or created // http://groups.google.com/group/sockjs/browse_thread/thread/71dfdff6e8f1e5f7 // Can't use beforeFilter, see comment of pathPrefix at the top of this controller. protected def handleCookie(): Unit = { val sockJsClassAndOptions = Config.routes.sockJsRouteMap.lookup(pathPrefix) if (sockJsClassAndOptions.cookieNeeded) { val value = requestCookies.getOrElse("JSESSIONID", "dummy") val cookie = new DefaultCookie("JSESSIONID", value) responseCookies.append(cookie) } } protected def callbackParam(): Option[String] = { val paramName = if (handlerEnv.queryParams.isDefinedAt("c")) "c" else "callback" val ret = paramo(paramName) if (ret.isEmpty) { response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR) respondText("\"callback\" parameter required") } ret } } trait NonWebSocketSessionActorAction extends ActorAction with SockJsAction { protected def lookupNonWebSocketSessionActor(sessionId: String): Unit = { SockJsAction.actorRegistry ! Registry.Lookup(sessionId) } //---------------------------------------------------------------------------- /** * We should close a streaming request every 128KB messages was send. * The test server should have this limit decreased to 4KB. */ private[this] var streamingBytesSent = 0 /** * All the chunking transports are closed by the server after CHUNKED_RESPONSE_LIMIT * bytes was sent, in order to force client to GC and reconnect. The server doesn't have * to send "c" frame. * * @return false if the channel will be closed when the channel write completes */ protected def respondStreamingWithLimit(text: String, isEventSource: Boolean = false, index_handler: Option[(Int, ActorRef)] = None): Boolean = { // This is length in characters, not bytes, // but in this case the result doesn't have to be precise val size = text.length streamingBytesSent += size val (f, ret) = if (streamingBytesSent < SockJsAction.CHUNKED_RESPONSE_LIMIT) { val f = if (isEventSource) respondEventSource(text) else respondText(text) (f, true) } else { context.stop(self) val f = if (isEventSource) respondEventSource(text) else respondText(text) closeWithLastChunk() (f, false) } index_handler.foreach { case (index, handler) => NotificationToHandlerUtil.onComplete(f, index, handler, write = false) } ret } protected def closeWithLastChunk(index_handler: Option[(Int, ActorRef)] = None): Unit = { val f = respondLastChunk().addListener(ChannelFutureListener.CLOSE) index_handler.foreach { case (index, handler) => NotificationToHandlerUtil.onComplete(f, index, handler, write = false) } } } trait NonWebSocketSessionReceiverActorAction extends NonWebSocketSessionActorAction { protected var nonWebSocketSession: ActorRef = _ // Call lookupOrCreateNonWebSocketSessionActor and continue with this method. // Here, context.become(receiveNotification) may be called. protected def onLookupOrRecreateResult(newlyCreated: Boolean): Unit protected def receiveNotification: Receive protected def lookupOrCreateNonWebSocketSessionActor(sessionId: String): Unit = { // Try to lookup first, then create later val props = Props(new NonWebSocketSession(Some(self), pathPrefix, this)) SockJsAction.actorRegistry ! Registry.Register(sessionId, props) context.become({ case Registry.Found(`sessionId`, actorRef) => nonWebSocketSession = actorRef context.watch(nonWebSocketSession) onLookupOrRecreateResult(false) case Registry.Created(`sessionId`, actorRef) => nonWebSocketSession = actorRef context.watch(nonWebSocketSession) onLookupOrRecreateResult(true) }) } override def postStop(): Unit = { if (nonWebSocketSession != null) { context.unwatch(nonWebSocketSession) if (!isDoneResponding) nonWebSocketSession ! AbortFromReceiverClient } super.postStop() } } @GET("") class Greeting extends SockJsAction { def nLastTokensToRemoveFromPathInfo = 0 def execute(): Unit = { respondText("Welcome to SockJS!\n") } } @Last @GET(":iframe") class Iframe extends SockJsAction { def nLastTokensToRemoveFromPathInfo = 1 def execute(): Unit = { val iframe = param("iframe") if (iframe.startsWith("iframe") && iframe.endsWith(".html")) { setClientCacheAggressively() respondHtml(DocType.html5(

Don't panic!

This is a SockJS hidden iframe. It's used for cross domain magic.

)) } else { respondDefault404Page() } } } @GET("info") class InfoGET extends SockJsAction { def nLastTokensToRemoveFromPathInfo = 1 def execute(): Unit = { setNoClientCache() val sockJsClassAndOptions = Config.routes.sockJsRouteMap.lookup(pathPrefix) respondJson(Map( "websocket" -> sockJsClassAndOptions.websocket, "cookie_needed" -> sockJsClassAndOptions.cookieNeeded, "origins" -> Config.xitrum.response.corsAllowOrigins, "entropy" -> SockJsAction.entropy() )) } } @POST(":serverId/:sessionId/xhr") class XhrPollingReceive extends NonWebSocketSessionReceiverActorAction with SkipCsrfCheck { def nLastTokensToRemoveFromPathInfo = 3 def execute(): Unit = { val sessionId = param("sessionId") handleCookie() setNoClientCache() lookupOrCreateNonWebSocketSessionActor(sessionId) } protected def onLookupOrRecreateResult(newlyCreated: Boolean): Unit = { if (newlyCreated) { respondJs("o\n") } else { nonWebSocketSession ! SubscribeFromReceiverClient context.become(receiveSubscribeResult) } } private def receiveSubscribeResult: Receive = { case SubscribeResultToReceiverClientAnotherConnectionStillOpen => respondJs("c[2010,\"Another connection still open\"]\n") .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientClosed => respondJs("c[3000,\"Go away!\"]\n") .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientMessages(messages) => val json = SeriDeseri.toJson(messages) val quoted = SockJsAction.quoteUnicode(json) respondJs("a" + quoted + "\n") case SubscribeResultToReceiverClientWaitForMessage => context.become(receiveNotification) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondJs("c[2011,\"Server error\"]\n") .addListener(ChannelFutureListener.CLOSE) } protected override def receiveNotification: Receive = { case NotificationToReceiverClientMessage(index, message, handler) => val json = SeriDeseri.toJson(Seq(message)) val quoted = SockJsAction.quoteUnicode(json) NotificationToHandlerUtil.onComplete(respondJs("a" + quoted + "\n"), index, handler, write = true) case NotificationToReceiverClientHeartbeat => respondJs("h\n") case NotificationToReceiverClientClosed(index, handler) => NotificationToHandlerUtil.onComplete( respondJs("c[3000,\"Go away!\"]\n"), index, handler, write = false ).addListener(ChannelFutureListener.CLOSE) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondJs("c[2011,\"Server error\"]\n") .addListener(ChannelFutureListener.CLOSE) } } @POST(":serverId/:sessionId/xhr_send") class XhrSend extends NonWebSocketSessionActorAction with SkipCsrfCheck { def nLastTokensToRemoveFromPathInfo = 3 def execute(): Unit = { val body = request.content.toString(Config.xitrum.request.charset) if (body.isEmpty) { response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR) respondText("Payload expected.") return } SeriDeseri.fromJson[Seq[String]](body) match { case None => response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR) respondText("Broken JSON encoding.") case Some(messages) => // body: ["m1", "m2"] val sessionId = param("sessionId") lookupNonWebSocketSessionActor(sessionId) context.become { case Registry.NotFound(`sessionId`) => respondDefault404Page() case Registry.Found(`sessionId`, nonWebSocketSession) => nonWebSocketSession ! MessagesFromSenderClient(messages) response.setStatus(HttpResponseStatus.NO_CONTENT) response.headers.set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8") respond() } } } } @POST(":serverId/:sessionId/xhr_streaming") class XhrStreamingReceive extends NonWebSocketSessionReceiverActorAction with SkipCsrfCheck { def nLastTokensToRemoveFromPathInfo = 3 def execute(): Unit = { val sessionId = param("sessionId") handleCookie() setNoClientCache() // There's always 2KB prelude, even for immediate close frame HttpUtil.setTransferEncodingChunked(response, true) response.headers.set(HttpHeaderNames.CONTENT_TYPE, "application/javascript; charset=" + Config.xitrum.request.charset) respondBinary(Unpooled.wrappedBuffer(SockJsAction.H2K)) lookupOrCreateNonWebSocketSessionActor(sessionId) } protected def onLookupOrRecreateResult(newlyCreated: Boolean): Unit = { if (newlyCreated) { respondStreamingWithLimit("o\n") context.become(receiveNotification) } else { nonWebSocketSession ! SubscribeFromReceiverClient context.become(receiveSubscribeResult) } } private def receiveSubscribeResult: Receive = { case SubscribeResultToReceiverClientAnotherConnectionStillOpen => respondJs("c[2010,\"Another connection still open\"]\n") closeWithLastChunk() case SubscribeResultToReceiverClientClosed => respondJs("c[3000,\"Go away!\"]\n") closeWithLastChunk() case SubscribeResultToReceiverClientMessages(messages) => val json = SeriDeseri.toJson(messages) val quoted = SockJsAction.quoteUnicode(json) if (respondStreamingWithLimit("a" + quoted + "\n")) context.become(receiveNotification) case SubscribeResultToReceiverClientWaitForMessage => context.become(receiveNotification) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondJs("c[2011,\"Server error\"]\n") closeWithLastChunk() } protected override def receiveNotification: Receive = { case NotificationToReceiverClientMessage(index, message, handler) => val json = SeriDeseri.toJson(Seq(message)) val quoted = SockJsAction.quoteUnicode(json) respondStreamingWithLimit("a" + quoted + "\n", isEventSource = false, Some(index, handler)) case NotificationToReceiverClientHeartbeat => respondStreamingWithLimit("h\n") case NotificationToReceiverClientClosed(index, handler) => respondJs("c[3000,\"Go away!\"]\n").addListener(new ChannelFutureListener { def operationComplete(f: ChannelFuture): Unit = { if (f.isSuccess) closeWithLastChunk(Some(index, handler)) else NotificationToHandlerUtil.onComplete(f, index, handler, write = false) } }) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondJs("c[2011,\"Server error\"]\n") closeWithLastChunk() } } @GET(":serverId/:sessionId/htmlfile") class HtmlFileReceive extends NonWebSocketSessionReceiverActorAction { def nLastTokensToRemoveFromPathInfo = 3 var callback: String = _ def execute(): Unit = { val callbacko = callbackParam() if (callbacko.isEmpty) return callback = callbacko.get val sessionId = param("sessionId") handleCookie() setNoClientCache() lookupOrCreateNonWebSocketSessionActor(sessionId) } protected def onLookupOrRecreateResult(newlyCreated: Boolean): Unit = { if (newlyCreated) { HttpUtil.setTransferEncodingChunked(response, true) respondHtml(SockJsAction.htmlFile(callback, with1KSpaces = true)) respondText("\r\n") context.become(receiveNotification) } else { nonWebSocketSession ! SubscribeFromReceiverClient context.become(receiveSubscribeResult) } } private def receiveSubscribeResult: Receive = { case SubscribeResultToReceiverClientAnotherConnectionStillOpen => respondHtml( SockJsAction.htmlFile(callback, with1KSpaces = false) + "\r\n" ) .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientClosed => respondHtml( SockJsAction.htmlFile(callback, with1KSpaces = false) + "\r\n" ) .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientMessages(messages) => val buffer = new StringBuilder val json = SeriDeseri.toJson(messages) val quoted = SockJsAction.quoteUnicode(json) buffer.append("\r\n") HttpUtil.setTransferEncodingChunked(response, true) respondHtml(SockJsAction.htmlFile(callback, with1KSpaces = true)) if (respondStreamingWithLimit(buffer.toString)) context.become(receiveNotification) case SubscribeResultToReceiverClientWaitForMessage => HttpUtil.setTransferEncodingChunked(response, true) respondHtml(SockJsAction.htmlFile(callback, with1KSpaces = true)) context.become(receiveNotification) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondHtml( SockJsAction.htmlFile(callback, with1KSpaces = false) + "\r\n" ) .addListener(ChannelFutureListener.CLOSE) } protected override def receiveNotification: Receive = { case NotificationToReceiverClientMessage(index, message, handler) => val buffer = new StringBuilder val json = SeriDeseri.toJson(Seq(message)) val quoted = SockJsAction.quoteUnicode(json) buffer.append("\r\n") respondStreamingWithLimit(buffer.toString, isEventSource = false, Some(index, handler)) case NotificationToReceiverClientHeartbeat => respondStreamingWithLimit("\r\n") case NotificationToReceiverClientClosed(index, handler) => respondHtml( SockJsAction.htmlFile(callback, with1KSpaces = false) + "\r\n" ).addListener(new ChannelFutureListener { def operationComplete(f: ChannelFuture): Unit = { if (f.isSuccess) closeWithLastChunk(Some(index, handler)) else NotificationToHandlerUtil.onComplete(f, index, handler, write = false) } }) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondHtml( SockJsAction.htmlFile(callback, with1KSpaces = false) + "\r\n" ) closeWithLastChunk() } } @GET(":serverId/:sessionId/jsonp") class JsonPPollingReceive extends NonWebSocketSessionReceiverActorAction { def nLastTokensToRemoveFromPathInfo = 3 var callback: String = _ def execute(): Unit = { val callbacko = callbackParam() if (callbacko.isEmpty) return callback = callbacko.get val sessionId = param("sessionId") handleCookie() setNoClientCache() lookupOrCreateNonWebSocketSessionActor(sessionId) } protected def onLookupOrRecreateResult(newlyCreated: Boolean): Unit = { if (newlyCreated) { respondJs(callback + "(\"o\");\r\n") } else { nonWebSocketSession ! SubscribeFromReceiverClient context.become(receiveSubscribeResult) } } private def receiveSubscribeResult: Receive = { case SubscribeResultToReceiverClientAnotherConnectionStillOpen => respondJs(callback + "(\"c[2010,\\\"Another connection still open\\\"]\");\r\n") .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientClosed => respondJs(callback + "(\"c[3000,\\\"Go away!\\\"]\");\r\n") .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientMessages(messages) => val buffer = new StringBuilder val json = SeriDeseri.toJson(messages) val quoted = SockJsAction.quoteUnicode(json) buffer.append(callback + "(\"a") buffer.append(jsEscape(quoted)) buffer.append("\");\r\n") respondJs(buffer.toString) case SubscribeResultToReceiverClientWaitForMessage => context.become(receiveNotification) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondJs(callback + "(\"c[2011,\\\"Server error\\\"]\");\r\n") .addListener(ChannelFutureListener.CLOSE) } protected override def receiveNotification: Receive = { case NotificationToReceiverClientMessage(index, message, handler) => val buffer = new StringBuilder val json = SeriDeseri.toJson(Seq(message)) val quoted = SockJsAction.quoteUnicode(json) buffer.append(callback + "(\"a") buffer.append(jsEscape(quoted)) buffer.append("\");\r\n") NotificationToHandlerUtil.onComplete(respondJs(buffer.toString), index, handler, write = true) case NotificationToReceiverClientHeartbeat => respondJs(callback + "(\"h\");\r\n") case NotificationToReceiverClientClosed(index, handler) => NotificationToHandlerUtil.onComplete( respondJs(callback + "(\"c[3000,\\\"Go away!\\\"]\");\r\n"), index, handler, write = false ).addListener(ChannelFutureListener.CLOSE) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondJs(callback + "(\"c[2011,\\\"Server error\\\"]\");\r\n") .addListener(ChannelFutureListener.CLOSE) } } @POST(":serverId/:sessionId/jsonp_send") class JsonPPollingSend extends NonWebSocketSessionActorAction with SkipCsrfCheck { def nLastTokensToRemoveFromPathInfo = 3 def execute(): Unit = { val body: String = try { val contentType = request.headers.get(HttpHeaderNames.CONTENT_TYPE) if (contentType != null && contentType.toLowerCase.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString)) { param("d") } else { request.content.toString(Config.xitrum.request.charset) } } catch { case NonFatal(e) => "" } handleCookie() if (body.isEmpty) { response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR) respondText("Payload expected.") return } SeriDeseri.fromJson[Seq[String]](body) match { case None => response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR) respondText("Broken JSON encoding.") case Some(messages) => // body: ["m1", "m2"] val sessionId = param("sessionId") lookupNonWebSocketSessionActor(sessionId) context.become { case Registry.NotFound(`sessionId`) => respondDefault404Page() case Registry.Found(`sessionId`, nonWebSocketSession) => nonWebSocketSession ! MessagesFromSenderClient(messages) // Konqueror does weird things on 204. // As a workaround we need to respond with something - let it be the string "ok". setNoClientCache() respondText("ok") } } } } @GET(":serverId/:sessionId/eventsource") class EventSourceReceive extends NonWebSocketSessionReceiverActorAction { def nLastTokensToRemoveFromPathInfo = 3 def execute(): Unit = { val sessionId = param("sessionId") handleCookie() setNoClientCache() lookupOrCreateNonWebSocketSessionActor(sessionId) } protected def onLookupOrRecreateResult(newlyCreated: Boolean): Unit = { if (newlyCreated) { respondEventSource("o") context.become(receiveNotification) } else { nonWebSocketSession ! SubscribeFromReceiverClient context.become(receiveSubscribeResult) } } private def receiveSubscribeResult: Receive = { case SubscribeResultToReceiverClientAnotherConnectionStillOpen => respondJs("c[2010,\"Another connection still open\"]\n") .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientClosed => respondJs("c[3000,\"Go away!\"]\n") .addListener(ChannelFutureListener.CLOSE) case SubscribeResultToReceiverClientMessages(messages) => val json = "a" + SeriDeseri.toJson(messages) val quoted = SockJsAction.quoteUnicode(json) if (respondStreamingWithLimit(quoted, isEventSource = true)) context.become(receiveNotification) case SubscribeResultToReceiverClientWaitForMessage => context.become(receiveNotification) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondJs("c[2011,\"Server error\"]\n") .addListener(ChannelFutureListener.CLOSE) } protected override def receiveNotification: Receive = { case NotificationToReceiverClientMessage(index, message, handler) => val json = "a" + SeriDeseri.toJson(Seq(message)) val quoted = SockJsAction.quoteUnicode(json) respondStreamingWithLimit(quoted, isEventSource = true, Some(index, handler)) case NotificationToReceiverClientHeartbeat => respondStreamingWithLimit("h", isEventSource = true) case NotificationToReceiverClientClosed(index, handler) => respondJs("c[3000,\"Go away!\"]\n") closeWithLastChunk(Some(index, handler)) case Terminated(actorRef) if actorRef == nonWebSocketSession => respondEventSource("c[2011,\"Server error\"]") closeWithLastChunk() } } // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-52 @Last @GET(":serverId/:sessionId/websocket") class WebSocketGET extends SockJsAction { def nLastTokensToRemoveFromPathInfo = 3 def execute(): Unit = { response.setStatus(HttpResponseStatus.BAD_REQUEST) respondText("""'Can "Upgrade" only to "WebSocket".'""") } } // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-54 // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-6 @Last @POST(":serverId/:sessionId/websocket") class WebSocketPOST extends SockJsAction with SkipCsrfCheck { def nLastTokensToRemoveFromPathInfo = 3 def execute(): Unit = { response.setStatus(HttpResponseStatus.METHOD_NOT_ALLOWED) response.headers.set(HttpHeaderNames.ALLOW, "GET") respond() } } // sessionId is ignored @WEBSOCKET(":serverId/:sessionId/websocket") class WebSocket extends WebSocketAction with ServerIdSessionIdValidator with SockJsPrefix { def nLastTokensToRemoveFromPathInfo = 3 private[this] var sockJsActorRef: ActorRef = _ def execute(): Unit = { sockJsActorRef = Config.routes.sockJsRouteMap.createSockJsAction(pathPrefix) respondWebSocketText("o") sockJsActorRef ! (self, currentAction) context.setReceiveTimeout(SockJsAction.TIMEOUT_HEARTBEAT) context.become { case ReceiveTimeout => respondWebSocketText("h") case WebSocketText(body) => // Server must ignore empty messages // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-69 if (body.nonEmpty) { // body: can be ["m1", "m2"] or "m1" // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-61 val normalizedBody = if (body.startsWith("[")) body else "[" + body + "]" SeriDeseri.fromJson[Seq[String]](normalizedBody) match { case None => // No c frame is sent! // http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-72 //respondWebSocketText("c[2011,\"Broken JSON encoding.\"]") //.addListener(ChannelFutureListener.CLOSE) respondWebSocketClose() case Some(messages) => messages.foreach { msg => sockJsActorRef ! SockJsText(msg) } } } case MessageFromHandler(index, text) => val json = SeriDeseri.toJson(Seq(text)) NotificationToHandlerUtil.onComplete(respondWebSocketText("a" + json), index, sockJsActorRef, write = true) case CloseFromHandler(index) => respondWebSocketText("c[3000,\"Go away!\"]").addListener(new ChannelFutureListener { def operationComplete(f: ChannelFuture): Unit = { if (f.isSuccess) NotificationToHandlerUtil.onComplete(respondWebSocketClose(), index, sockJsActorRef, write = false) else NotificationToHandlerUtil.onComplete(f, index, sockJsActorRef, write = false) } }) } } override def postStop(): Unit = { if (sockJsActorRef != null) Config.actorSystem.stop(sockJsActorRef) super.postStop() } } @WEBSOCKET("websocket") class RawWebSocket extends WebSocketAction with ServerIdSessionIdValidator with SockJsPrefix { def nLastTokensToRemoveFromPathInfo = 1 private[this] var sockJsActorRef: ActorRef = _ def execute(): Unit = { sockJsActorRef = Config.routes.sockJsRouteMap.createSockJsAction(pathPrefix) sockJsActorRef ! (self, currentAction) context.become { case WebSocketText(text) => sockJsActorRef ! SockJsText(text) case MessageFromHandler(index, text) => NotificationToHandlerUtil.onComplete(respondWebSocketText(text), index, sockJsActorRef, write = true) case CloseFromHandler(index) => NotificationToHandlerUtil.onComplete(respondWebSocketClose(), index, sockJsActorRef, write = false) } } override def postStop(): Unit = { if (sockJsActorRef != null) Config.actorSystem.stop(sockJsActorRef) super.postStop() } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy