Search code examples
javascriptrubywebsocketem-websocket

WebSocket handshake with Ruby and EM::WebSocket::Server


I am trying to create a simple WebSocket connection in JavaScript against my Rails app. I get the following:

WebSocket connection to 'ws://localhost:4000/' failed: Error during WebSocket handshake: 'Sec-WebSocket-Accept' header is missing

What am I doing wrong? Here is my code:

JavaScript:

var socket = new WebSocket('ws://localhost:4000');

socket.onopen = function() {
  var handshake =
    "GET / HTTP/1.1\n" +
    "Host: localhost\n" +
    "Upgrade: websocket\n" +
    "Connection: Upgrade\n" +
    "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\n" +
    "Sec-WebSocket-Protocol: quote\n" +
    "Sec-WebSocket-Version: 13\n" +
    "Origin: http://localhost\n";

  socket.send(handshake);
};

socket.onmessage = function(data) {
  console.log(data);
};

Ruby:

require 'rubygems'
require 'em-websocket-server'

module QuoteService
  class WebSocket < EventMachine::WebSocket::Server
    def on_connect
      handshake_response =  "HTTP/1.1 101 Switching Protocols\n"
      handshake_response << "Upgrade: websocket\n"
      handshake_response << "Connection: Upgrade\n"
      handshake_response << "Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\n"
      handshake_response << "Sec-WebSocket-Protocol: quote\n"

      send_message(handshake_response)
    end

    def on_receive(data)
      puts 'RECEIVED: ' + data
    end
  end
end

EventMachine.run do
  print 'Starting WebSocket server...'
  EventMachine.start_server '0.0.0.0', 4000, QuoteService::WebSocket
  puts 'running'
end

The handshake headers are per Wikipedia.


Solution

    1. I think that once the connection is open the request and response have already occurred, so sending headers at that point is too late. In addition, headers have to end with a blank line, which you omitted.

    2. According to the demos, you don't even have to set headers in the client or the server--the ruby module automatically takes care of the headers on the server side, and html5 automatically takes care of the headers on the client side. I think this should work:

      require "em-websocket-server"

      class EchoServer < EM::WebSocket::Server

      def on_connect EM::WebSocket::Log.debug "Connected" puts "I felt a connection." end

      def on_receive msg puts "RECEIVED: #{msg}" send_message msg end

      end

      EM.run do myhost = "0.0.0.0" myport = 8000 puts "Starting WebSocket server. Listening on port #{myport}..." EM.start_server myhost, myport, EchoServer end

    html file:

    <!DOCTYPE html> <html> <head><title>Test</title>
    
    <script type="text/javascript">
    
      var myWebSocket = new WebSocket("ws://localhost:8000");
    
      myWebSocket.onopen = function(evt)    { 
        console.log("Connection open. Sending message..."); 
        myWebSocket.send("Hello WebSockets!");       };
    
      myWebSocket.onmessage = function(evt)    { 
        console.log(evt.data);
        myWebSocket.close();   };
    
      myWebSocket.onclose = function(evt)    { 
        console.log("Connection closed.");    };
    
      myWebSocket.onerror = function(err)   {
        alert(err.name + " => " + err.message);   } </script>
    
    </head> <body>   <div>Hello</div> </body> </html>
    

    And it does work in Safari 5.1.9 (which is an older browser): I see the expected output on both the server and the client. However, the code does not work in Firefox 21: I get the error message...

    Firefox can't establish a connection to the server at ws://localhost:8000/.
        var myWebSocket = new WebSocket("ws://localhost:8000");
    

    I notice that in both Firebug and Safari Developer Tools, the server does not send a Sec-WebSocket-Accept header:

    Response Headers
    
    Connection          Upgrade
    Upgrade         WebSocket
    WebSocket-Location  ws://localhost:8000/
    WebSocket-Origin    null
    
    
    Request Headers
    
    Accept                  text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Encoding         gzip, deflate
    Accept-Language         en-US,en;q=0.5
    Cache-Control           no-cache
    Connection          keep-alive, Upgrade
    DNT                 1
    Host                    localhost:8000
    Origin                  null
    Pragma                  no-cache
    Sec-WebSocket-Key   r9xT+ywe533EHF09wxelkg==
    Sec-WebSocket-Version   13
    Upgrade                 websocket
    User-Agent          Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:21.0) Gecko/20100101 Firefox/21.0
    

    Nothing I tried would make the code work in Firefox 21.0. To check whether Firefox 21.0 even supports websockets, I went to:

    http://www.websocket.org/echo.html  
    

    and it said my browser does support websockets.

    1. Is there any reason you have to use the em-websocket-server module? The last modification for that module on github was three years ago. And whenever you see require rubygems in ruby code, that should alert you that the code is old. I tried the newer em-websocket module, and I was able to successfully transfer data back and forth using websockets on both Firefox 21.0 and Safari 5.1.9:

      require 'em-websocket'

      myhost = "0.0.0.0" myport = 8000

      EM.run { puts "Listening on port #{myport}..."

      EM::WebSocket.run(:host => myhost, :port => myport, :debug => false) do |ws|

       ws.onopen do |handshake|
         path = handshake.path
         query_str = handshake.query
         origin = handshake.origin
      
         puts "WebSocket opened:"
         puts "\t path  \t\t -> #{path}" 
         puts "\t query_str \t -> #{query_str}"
         puts "\t origin \t -> #{origin}"
       end 
      
       ws.onmessage { |msg|
         ws.send "Pong: #{msg}"
       }
       ws.onclose {
         puts "WebSocket closed"
       }
       ws.onerror { |e|
         puts "Error: #{e.message}"
       }
      

      end }

    Same client side code. Now the response headers include Sec-WebSocket-Accept:

    Response Headers
    
    Connection          Upgrade
    Sec-WebSocket-Accept    LyIm6d+kAAqkcTR744tVK9HMepY=
    Upgrade                 websocket
    
    
    Request Headers
    
    Accept  text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Encoding gzip, deflate
    Accept-Language en-US,en;q=0.5
    Cache-Control   no-cache
    Connection  keep-alive, Upgrade
    DNT 1
    Host    localhost:8000
    Origin  null
    Pragma  no-cache
    Sec-WebSocket-Key   pbK8lFHQAF+arl9tFvHn/Q==
    Sec-WebSocket-Version   13
    Upgrade websocket
    User-Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:21.0) Gecko/20100101 Firefox/21.0
    

    In your code, I don't think you are setting any headers. Instead, you are just sending messages back and forth that happen to contain characters that look like headers. Apparently, your browser requires the Sec-WebSocket-Accept header in the response before it will allow the connection, and when the em-websocket-server module fails to set that header in the response, your browser refuses the connection.

    The relevant source code for em-websockets-server looks like this:

    module EM
      module WebSocket
        module Protocol
          module Version76
          
            # generate protocol 76 compatible response headers
            def response
              response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
              response << "Upgrade: WebSocket\r\n"
              response << "Connection: Upgrade\r\n"
              response << "Sec-WebSocket-Origin: #{origin}\r\n"
              response << "Sec-WebSocket-Location: #{scheme}://#{host}#{path}\r\n"
      
              if protocol
                response << "Sec-WebSocket-Protocol: #{protocol}\r\n"
              end
    
              response << "\r\n"
              response << Digest::MD5.digest(keyset)
    
              response
            end
    

    As you can see, it doesn't set the Sec-WebSocket-Accept header. That code is in a module called Version76, and searching google for websockets version 76 yields an obsolete protocol(which contains an example of a request and response):

    https://datatracker.ietf.org/doc/html/draft-hixie-thewebsocketprotocol-76

    Here is the current websockets protocol(which also contains an example of a request and response):

    https://www.rfc-editor.org/rfc/rfc6455

    Conclusion: em-websockets-server is obsolete.