Posted By

heri16 on 02/03/11


Tagged


Versions (?)

Play-Websocket Iteration 2


 / Published in: Java
 

  1. package play.server;
  2.  
  3. import org.jboss.netty.buffer.ChannelBuffer;
  4. import org.jboss.netty.buffer.ChannelBufferInputStream;
  5. import org.jboss.netty.buffer.ChannelBuffers;
  6. import org.jboss.netty.channel.*;
  7. import org.jboss.netty.handler.codec.http.*;
  8. import org.jboss.netty.handler.codec.http.websocket.*;
  9.  
  10. import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
  11.  
  12. import play.Invoker;
  13. import play.Logger;
  14. import play.Play;
  15. import play.PlayPlugin;
  16. import play.mvc.ActionInvoker;
  17. import play.mvc.Http;
  18. import play.mvc.Http.Request;
  19. import play.mvc.Http.Response;
  20.  
  21. import java.io.*;
  22. import java.util.logging.Level;
  23. import java.util.concurrent.ConcurrentMap;
  24. import java.util.concurrent.ConcurrentSkipListMap;
  25. import java.security.MessageDigest;
  26.  
  27. public class PlayWebsocketHandler extends PlayHandler {
  28.  
  29. // Specify ws:// or wss:// here
  30. private static final String WEBSOCKET_SCHEME = "ws://";
  31.  
  32. // Specify PlayRouter method prefix here
  33. private static final String WEBSOCKET_METHOD_NAME = "WEBSOCKET";
  34.  
  35. /* A map that stores netty-requests that has been upgraded (via handshake) to websocket stream.
  36.   * ConcurrentMap provides good lookup performance.
  37.   * ChannelHandlerContext can be used as reference, as it does not change on every messageReceived.
  38.   * Refer to http://docs.jboss.org/netty/3.1/api/org/jboss/netty/channel/ChannelHandlerContext.html#setAttachment
  39.   * Alternatively, client ip address may be used as key.
  40.   */
  41. private final ConcurrentMap<ChannelHandlerContext, HttpRequest> activeWebsocketMap = new ConcurrentSkipListMap<ChannelHandlerContext, HttpRequest>();
  42.  
  43. @Override
  44. public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
  45. Logger.trace("messageReceived: begin");
  46.  
  47. final Object msg = e.getMessage();
  48. if (msg instanceof WebSocketFrame) {
  49. final WebSocketFrame nettyWebSocketFrame = (WebSocketFrame) msg;
  50.  
  51. // This is all that is needed to send arbitrary content over the wire.
  52. // ctx.getChannel().write(new DefaultWebSocketFrame(nettyWebSocketFrame.getTextData().toUpperCase()));
  53.  
  54. // Websocket frames do not give netty requests, and thus need to be retrieved from the handshake.
  55. HttpRequest nettyRequest = activeWebsocketMap.get(ctx);
  56. // Faster version of the above
  57. //HttpRequest nettyRequest = (HttpRequest) ctx.getAttachment();
  58.  
  59. try {
  60. // Needed so parseRequest() will work properly
  61. nettyRequest.setHeader("X-HTTP-Method-Override", WEBSOCKET_METHOD_NAME);
  62. ChannelBuffer b = nettyWebSocketFrame.getBinaryData();
  63. nettyRequest.setContent(b);
  64.  
  65. // Should optimize to avoid doing parsing on every frame
  66. Request request = parseRequest(ctx, nettyRequest);
  67. request = processRequest(request);
  68.  
  69. // More optimized version than doing setContent() for parseRequest()
  70. request.body = new ChannelBufferInputStream(b);
  71. //
  72.  
  73. final Response response = new Response();
  74.  
  75. Http.Response.current.set(response);
  76. response.out = new ByteArrayOutputStream();
  77. boolean raw = false;
  78. for (PlayPlugin plugin : Play.plugins) {
  79. if (plugin.rawInvocation(request, response)) {
  80. raw = true;
  81. break;
  82. }
  83. }
  84. if (raw) {
  85. copyResponse(ctx, request, response, nettyRequest);
  86. } else {
  87. Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e));
  88. }
  89.  
  90. } catch (Exception ex) {
  91. serve500(ex, ctx, nettyRequest);
  92. }
  93.  
  94. } else if (msg instanceof HttpRequest) {
  95. final HttpRequest nettyRequest = (HttpRequest) msg;
  96.  
  97. if (HttpHeaders.Values.UPGRADE.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.CONNECTION)) &&
  98. HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.UPGRADE))) {
  99.  
  100. // Do upgrade handshake.
  101. handleWebSocketHandshakeRequest(ctx, nettyRequest);
  102. // Keep nettyRequest reference in Map.
  103. activeWebsocketMap.put(ctx, nettyRequest);
  104. // Faster version of the above
  105. //ctx.setAttachment(nettyRequest);
  106.  
  107. }
  108.  
  109. } else {
  110. // Not a websocket frame nor upgrade request, pass to PlayHandler.
  111. super.messageReceived(ctx, e);
  112. }
  113.  
  114. Logger.trace("messageReceived: end");
  115. }
  116.  
  117. /*
  118.   * Returns the websocket endpoint's Uniform Resource Identifier (URI)
  119.   */
  120. private static String getWebSocketURI(HttpRequest nettyRequest) {
  121. return WEBSOCKET_SCHEME + nettyRequest.getHeader(HttpHeaders.Names.HOST) + nettyRequest.getUri();
  122. }
  123.  
  124. /*
  125.   * This method is an almost exact copy of Netty's sample-code for Websocket.
  126.   * This is a proven implementation. You do not need to look into it.
  127.   */
  128. private static void handleWebSocketHandshakeRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest) {
  129. // Create the WebSocket handshake response.
  130. HttpResponse res = new DefaultHttpResponse(
  131. HttpVersion.HTTP_1_1,
  132. new HttpResponseStatus(101, "Web Socket Protocol Handshake"));
  133. res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET);
  134. res.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE);
  135.  
  136. // Fill in the headers and contents depending on handshake method.
  137. if (nettyRequest.containsHeader(SEC_WEBSOCKET_KEY1) &&
  138. nettyRequest.containsHeader(SEC_WEBSOCKET_KEY2)) {
  139. // New handshake method with a challenge:
  140. res.addHeader(SEC_WEBSOCKET_ORIGIN, nettyRequest.getHeader(ORIGIN));
  141. res.addHeader(SEC_WEBSOCKET_LOCATION, getWebSocketURI(nettyRequest));
  142. String protocol = nettyRequest.getHeader(SEC_WEBSOCKET_PROTOCOL);
  143. if (protocol != null) {
  144. res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
  145. }
  146.  
  147. // Calculate the answer of the challenge.
  148. String key1 = nettyRequest.getHeader(SEC_WEBSOCKET_KEY1);
  149. String key2 = nettyRequest.getHeader(SEC_WEBSOCKET_KEY2);
  150. int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
  151. int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
  152. long c = nettyRequest.getContent().readLong();
  153. ChannelBuffer input = ChannelBuffers.buffer(16);
  154. input.writeInt(a);
  155. input.writeInt(b);
  156. input.writeLong(c);
  157. byte[] digest = null;
  158. try {
  159. digest = MessageDigest.getInstance("MD5").digest(input.array());
  160. } catch (Exception ex) {
  161. java.util.logging.Logger.getLogger(PlayHandler.class.getName()).log(Level.SEVERE, null, ex);
  162. }
  163. ChannelBuffer output = ChannelBuffers.wrappedBuffer(digest);
  164. res.setContent(output);
  165. } else {
  166. // Old handshake method with no challenge:
  167. res.addHeader(WEBSOCKET_ORIGIN, nettyRequest.getHeader(ORIGIN));
  168. res.addHeader(WEBSOCKET_LOCATION, getWebSocketURI(nettyRequest));
  169. String protocol = nettyRequest.getHeader(WEBSOCKET_PROTOCOL);
  170. if (protocol != null) {
  171. res.addHeader(WEBSOCKET_PROTOCOL, protocol);
  172. }
  173. }
  174.  
  175. // Upgrade the connection and send the handshake response.
  176. ChannelPipeline p = ctx.getChannel().getPipeline();
  177. p.remove("aggregator");
  178. p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder());
  179.  
  180. ctx.getChannel().write(res);
  181.  
  182. p.replace("encoder", "wsencoder", new WebSocketFrameEncoder());
  183. }
  184.  
  185. }

Report this snippet  

You need to login to post a comment.