em-websocket-0.5.1/0000755000000000000000000000000013326125113012575 5ustar rootrootem-websocket-0.5.1/README.md0000664000000000000000000001647313326125113014071 0ustar rootroot# EM-WebSocket [![Gem Version](https://badge.fury.io/rb/em-websocket.png)](http://rubygems.org/gems/em-websocket) [![Analytics](https://ga-beacon.appspot.com/UA-71196-10/em-websocket/readme)](https://github.com/igrigorik/ga-beacon) EventMachine based, async, Ruby WebSocket server. Take a look at examples directory, or check out the blog post: [Ruby & Websockets: TCP for the Web](http://www.igvita.com/2009/12/22/ruby-websockets-tcp-for-the-browser/). ## Simple server example ```ruby require 'em-websocket' EM.run { EM::WebSocket.run(:host => "0.0.0.0", :port => 8080) do |ws| ws.onopen { |handshake| puts "WebSocket connection open" # Access properties on the EM::WebSocket::Handshake object, e.g. # path, query_string, origin, headers # Publish message to the client ws.send "Hello Client, you connected to #{handshake.path}" } ws.onclose { puts "Connection closed" } ws.onmessage { |msg| puts "Recieved message: #{msg}" ws.send "Pong: #{msg}" } end } ``` ## Protocols supported, and protocol specific functionality Supports all WebSocket protocols in use in the wild (and a few that are not): drafts 75, 76, 1-17, rfc. While some of the changes between protocols are unimportant from the point of view of application developers, a few drafts did introduce new functionality. It's possible to easily test for this functionality by using ### Ping & pong supported Call `ws.pingable?` to check whether ping & pong is supported by the protocol in use. It's possible to send a ping frame (`ws.ping(body = '')`), which the client must respond to with a pong, or the server can send an unsolicited pong frame (`ws.pong(body = '')`) which the client should not respond to. These methods can be used regardless of protocol version; they return true if the protocol supports ping&pong or false otherwise. When receiving a ping, the server will automatically respond with a pong as the spec requires (so you should _not_ write an onping handler that replies with a pong), however it is possible to bind to ping & pong events if desired by using the `onping` and `onpong` methods. ### Close codes and reasons A WebSocket connection can be closed cleanly, regardless of protocol, by calling `ws.close(code = nil, body = nil)`. Early protocols just close the TCP connection, draft 3 introduced a close handshake, and draft 6 added close codes and reasons to the close handshake. Call `ws.supports_close_codes?` to check whether close codes are supported (i.e. the protocol version is 6 or above). The `onclose` callback is passed a hash which may contain following keys (depending on the protocol version): * `was_clean`: boolean indicating whether the connection was closed via the close handshake. * `code`: the close code. There are two special close codes which the server may set (as defined in the WebSocket spec): * 1005: no code was supplied * 1006: abnormal closure (the same as `was_clean: false`) * `reason`: the close reason Acceptable close codes are defined in the WebSocket rfc (). The following codes can be supplies when calling `ws.close(code)`: * 1000: a generic normal close * range 3xxx: reserved for libraries, frameworks, and applications (and can be registered with IANA) * range 4xxx: for private use If unsure use a code in the 4xxx range. em-websocket may also close a connection with one of the following close codes: * 1002: WebSocket protocol error. * 1009: Message too big to process. By default em-websocket will accept frames up to 10MB in size. If a frame is larger than this the connection will be closed without reading the frame data. The limit can be overriden globally (`EM::WebSocket.max_frame_size = bytes`) or on a specific connection (`ws.max_frame_size = bytes`). ## Secure server It is possible to accept secure `wss://` connections by passing `:secure => true` when opening the connection. Pass a `:tls_options` hash containing keys as described in http://eventmachine.rubyforge.org/EventMachine/Connection.html#start_tls-instance_method **Warning**: Safari 5 does not currently support prompting on untrusted SSL certificates therefore using a self signed certificate may leave you scratching your head. ```ruby EM::WebSocket.start({ :host => "0.0.0.0", :port => 443, :secure => true, :tls_options => { :private_key_file => "/private/key", :cert_chain_file => "/ssl/certificate" } }) do |ws| # ... end ``` It's possible to check whether an incoming connection is secure by reading `handshake.secure?` in the onopen callback. ## Running behind an SSL Proxy/Terminator, like Stunnel The `:secure_proxy => true` option makes it possible to use em-websocket behind a secure SSL proxy/terminator like [Stunnel](http://www.stunnel.org/) which does the actual encryption & decryption. Note that this option is only required to support drafts 75 & 76 correctly (e.g. Safari 5.1.x & earlier, and Safari on iOS 5.x & earlier). ```ruby EM::WebSocket.start({ :host => "0.0.0.0", :port => 8080, :secure_proxy => true }) do |ws| # ... end ``` ## Handling errors There are two kinds of errors that need to be handled -- WebSocket protocol errors and errors in application code. WebSocket protocol errors (for example invalid data in the handshake or invalid message frames) raise errors which descend from `EM::WebSocket::WebSocketError`. Such errors are rescued internally and the WebSocket connection will be closed immediately or an error code sent to the browser in accordance to the WebSocket specification. It is possible to be notified in application code of such errors by including an `onerror` callback. ```ruby ws.onerror { |error| if error.kind_of?(EM::WebSocket::WebSocketError) # ... end } ``` Application errors are treated differently. If no `onerror` callback has been defined these errors will propagate to the EventMachine reactor, typically causing your program to terminate. If you wish to handle exceptions, simply supply an `onerror callback` and check for exceptions which are not descendant from `EM::WebSocket::WebSocketError`. It is also possible to log all errors when developing by including the `:debug => true` option when initialising the WebSocket server. ## Emulating WebSockets in older browsers It is possible to emulate WebSockets in older browsers using flash emulation. For example take a look at the [web-socket-js](https://github.com/gimite/web-socket-js) project. Using flash emulation does require some minimal support from em-websocket which is enabled by default. If flash connects to the WebSocket port and requests a policy file (which it will do if it fails to receive a policy file on port 843 after a timeout), em-websocket will return one. Also see for an example policy file server which you can run on port 843. ## Examples & Projects using em-websocket * [Pusher](http://pusher.com) - Realtime Messaging Service * [Livereload](https://github.com/mockko/livereload) - LiveReload applies CSS/JS changes to Safari or Chrome w/o reloading * [Twitter AMQP WebSocket Example](http://github.com/rubenfonseca/twitter-amqp-websocket-example) * examples/multicast.rb - broadcast all ruby tweets to all subscribers * examples/echo.rb - server <> client exchange via a websocket # License The MIT License - Copyright (c) 2009-2013 Ilya Grigorik, Martyn Loughran em-websocket-0.5.1/CHANGELOG.rdoc0000664000000000000000000001137013326125113014741 0ustar rootroot= Changelog == 0.5.1 / 2014-04-23 - new features: - Support for receiving binary messages - changed: - Allow additional close codes to be sent by apps - Raise better errors on missing Sec-WebSocket-Key2 - Updated http_parser.rb dependency to 0.6.0 - bug fixes: - Abort if HTTP request URI is invalid - Force close connections that have been sent a close handshake after a timeout - improved spec compliance on: - Missing continuation frames - Fragmented control frames - Close behaviour after protocol errors == 0.5.0 / 2013-03-05 - new features: - onclose handler is now passed a hash containing was_clean (set to true in drafts 03 and above when a connection is closed with a closing handshake, either by the server or the client), the close code, and reason (drafts 06 and above). Close code 1005 indicates that no code was supplied, and 1006 that the connection was closed abnormally. - use Connection#support_close_codes? to easily check whether close codes are supported by the WebSocket protocol (drafts 06 and above) - closes connection with 1007 close code if text frame contains invalid UTF8 - added Handshake#secure? for checking whether the connection is secure (either ssl or behind an ssl proxy) - changed: - Defaults to sending no close code rather than 1000 (consistent with browsers) - Allows sending a 3xxx close code - Renamed Connection#close_websocket to Connection#close (again for consistency with browsers). Old method is available till 0.6. - Sends reasons with internally generated closure (previously only sent code) - Echos close code when replying to close handshake == 0.4.0 / 2013-01-22 - new features: - on_open handler is now passed a handshake object which exposes the request headers, path, and query parameters - Easily access the protocol version via Handshake#protocol_version - Easily access the origin via Handshake#origin - changed: - Removed Connection#request - change to using handshake passed to on_open - internals: - Uses the http_parser.rb gem == 0.3.8 / 2012-07-12 - bug fixes: - Fixed support for Ruby 1.8.7 which was broken in 0.3.7 == 0.3.7 / 2012-07-11 - new features: - Supports sending 1009 error code when incoming frame is too large to handle, and added associated exception class WSMessageTooBigError [Martyn Loughran] - Supports overriding the maximum frame size by setting the max_frame_size accessor on the connection object (in bytes). Default unchanged at 10MB. [Martyn Loughran] - bug fixes: - Fixes some encoding issues on Ruby 1.9 [Dingding Ye] - Raises a HandshakeError if WS header is empty [Markus Fenske] - Connection#send would mutate passed string to BINARY encoding. The fix still mutates the string by forcing the encoding back to UTF-8 before returning, but if the passed string was encoded as UTF-8 this is equivalent [Martyn Loughran] == 0.3.6 / 2011-12-23 - new features: - Supports sending ping & pong messages - Supports binding to received ping & pong messages == 0.3.5 / 2011-10-24 - new features: - Support WebSocket draft 13 == 0.3.2 / 2011-10-09 - bugfixes: - Handling of messages with > 2 frames - Encode string passed to onmessage handler as UTF-8 on Ruby 1.9 - Add 10MB frame length limit to all draft versions == 0.3.1 / 2011-07-28 - new features: - Support WebSocket drafts 07 & 08 == 0.3.0 / 2011-05-06 - new features: - Support WebSocket drafts 05 & 06 - changes: - Accept request headers in a case insensitive manner - Change handling of errors. Previously some application errors were caught internally and were invisible unless an onerror callback was supplied. See readme for details == 0.2.1 / 2011-03-01 - bugfixes: - Performance improvements to draft 76 framing - Limit frame lengths for draft 76 - Better error handling for draft 76 handshake - Ruby 1.9 support == 0.2.0 / 2010-11-23 - new features: - Support for WebSocket draft 03 - bugfixes: - Handle case when handshake split into two receive_data calls - Stricter regexp matching of frames == 0.1.4 / 2010-08-23 - new features: - Allow custom ssl certificate to be used by passing :tls_options - Protect against errors caused by non limited frame lengths - Use custom exceptions rather than RuntimeError - bugfixes: - Handle invalid HTTP request with HandshakeError == 0.1.3 / 2010-07-18 - new features: - onerror callback - bugfixes: - proper handling of zero spaces in key1 or key2(prevent ZeroDivisionError) - convert received data to utf-8 to prevent ruby 1.9 errors - fix handling of null bytes within a frame == 0.1.2 / 2010-06-16 - new features: - none - bugfixes: - allow $ character inside header key == 0.1.1 / 2010-06-13 - new features: - wss/ssl support - bugfixes: - can't & strings == 0.1.0 / 2010-06-12 - initial releaseem-websocket-0.5.1/Rakefile0000644000000000000000000000036113326125113014242 0ustar rootrootrequire 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new do |t| t.rspec_opts = ["-c", "-f progress", "-r ./spec/helper.rb"] t.pattern = 'spec/**/*_spec.rb' end task :default => :spec em-websocket-0.5.1/spec/0000755000000000000000000000000013326125113013527 5ustar rootrootem-websocket-0.5.1/spec/unit/0000755000000000000000000000000013326125113014506 5ustar rootrootem-websocket-0.5.1/spec/unit/masking_spec.rb0000644000000000000000000000153613326125113017503 0ustar rootroot# encoding: BINARY require 'helper' describe EM::WebSocket::MaskedString do it "should allow reading 4 byte mask and unmasking byte / bytes" do t = EM::WebSocket::MaskedString.new("\x00\x00\x00\x01\x00\x01\x00\x01") t.read_mask t.getbyte(3).should == 0x00 t.getbytes(4, 4).should == "\x00\x01\x00\x00" t.getbytes(5, 3).should == "\x01\x00\x00" end it "should return nil from getbyte if index requested is out of range" do t = EM::WebSocket::MaskedString.new("\x00\x00\x00\x00\x53") t.read_mask t.getbyte(4).should == 0x53 t.getbyte(5).should == nil end it "should allow switching masking on and off" do t = EM::WebSocket::MaskedString.new("\x02\x00\x00\x00\x03") t.getbyte(4).should == 0x03 t.read_mask t.getbyte(4).should == 0x01 t.unset_mask t.getbyte(4).should == 0x03 end end em-websocket-0.5.1/spec/unit/framing_spec.rb0000664000000000000000000001763213326125113017503 0ustar rootroot# encoding: BINARY require 'helper' describe EM::WebSocket::Framing03 do class FramingContainer include EM::WebSocket::Framing03 def initialize @connection = Object.new def @connection.max_frame_size 1000000 end end def <<(data) @data << data process_data end def debug(*args); end end before :each do @f = FramingContainer.new @f.initialize_framing end describe "basic examples" do it "connection close" do @f.should_receive(:message).with(:close, '', '') @f << 0b00000001 @f << 0b00000000 end it "ping" do @f.should_receive(:message).with(:ping, '', '') @f << 0b00000010 @f << 0b00000000 end it "pong" do @f.should_receive(:message).with(:pong, '', '') @f << 0b00000011 @f << 0b00000000 end it "text" do @f.should_receive(:message).with(:text, '', 'foo') @f << 0b00000100 @f << 0b00000011 @f << 'foo' end it "Text in two frames" do @f.should_receive(:message).with(:text, '', 'hello world') @f << 0b10000100 @f << 0b00000110 @f << "hello " @f << 0b00000000 @f << 0b00000101 @f << "world" end it "2 byte extended payload length text frame" do data = 'a' * 256 @f.should_receive(:message).with(:text, '', data) @f << 0b00000100 # Single frame, text @f << 0b01111110 # Length 126 (so read 2 bytes) @f << 0b00000001 # Two bytes in network byte order (256) @f << 0b00000000 @f << data end end # These examples are straight from the spec # http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03#section-4.6 describe "examples from the spec" do it "a single-frame text message" do @f.should_receive(:message).with(:text, '', 'Hello') @f << "\x04\x05Hello" end it "a fragmented text message" do @f.should_receive(:message).with(:text, '', 'Hello') @f << "\x84\x03Hel" @f << "\x00\x02lo" end it "Ping request and response" do @f.should_receive(:message).with(:ping, '', 'Hello') @f << "\x02\x05Hello" end it "256 bytes binary message in a single frame" do data = "a"*256 @f.should_receive(:message).with(:binary, '', data) @f << "\x05\x7E\x01\x00" + data end it "64KiB binary message in a single frame" do data = "a"*65536 @f.should_receive(:message).with(:binary, '', data) @f << "\x05\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data end end describe "other tests" do it "should accept a fragmented unmasked text message in 3 frames" do @f.should_receive(:message).with(:text, '', 'Hello world') @f << "\x84\x03Hel" @f << "\x80\x02lo" @f << "\x00\x06 world" end end describe "error cases" do it "should raise an exception on continuation frame without preceeding more frame" do lambda { @f << 0b00000000 # Single frame, continuation @f << 0b00000001 # Length 1 @f << 'f' }.should raise_error(EM::WebSocket::WebSocketError, 'Continuation frame not expected') end end end # These examples are straight from the spec # http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03#section-4.6 describe EM::WebSocket::Framing04 do class FramingContainer04 include EM::WebSocket::Framing04 def initialize @connection = Object.new def @connection.max_frame_size 1000000 end end def <<(data) @data << data process_data end def debug(*args); end end before :each do @f = FramingContainer04.new @f.initialize_framing end describe "examples from the spec" do it "a single-frame text message" do @f.should_receive(:message).with(:text, '', 'Hello') @f << "\x84\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello" end it "a fragmented text message" do @f.should_receive(:message).with(:text, '', 'Hello') @f << "\x04\x03Hel" @f << "\x80\x02lo" end it "Ping request" do @f.should_receive(:message).with(:ping, '', 'Hello') @f << "\x82\x05Hello" end it "a pong response" do @f.should_receive(:message).with(:pong, '', 'Hello') @f << "\x83\x05Hello" end it "256 bytes binary message in a single frame" do data = "a"*256 @f.should_receive(:message).with(:binary, '', data) @f << "\x85\x7E\x01\x00" + data end it "64KiB binary message in a single frame" do data = "a"*65536 @f.should_receive(:message).with(:binary, '', data) @f << "\x85\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data end end describe "other tests" do it "should accept a fragmented unmasked text message in 3 frames" do @f.should_receive(:message).with(:text, '', 'Hello world') @f << "\x04\x03Hel" @f << "\x00\x02lo" @f << "\x80\x06 world" end end end describe EM::WebSocket::Framing07 do class FramingContainer07 include EM::WebSocket::Framing07 def initialize @connection = Object.new def @connection.max_frame_size 1000000 end end def <<(data) @data << data process_data end def debug(*args); end end before :each do @f = FramingContainer07.new @f.initialize_framing end # These examples are straight from the spec # http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07#section-4.6 describe "examples from the spec" do it "a single-frame unmakedtext message" do @f.should_receive(:message).with(:text, '', 'Hello') @f << "\x81\x05\x48\x65\x6c\x6c\x6f" # "\x84\x05Hello" end it "a single-frame masked text message" do @f.should_receive(:message).with(:text, '', 'Hello') @f << "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" # "\x84\x05Hello" end it "a fragmented unmasked text message" do @f.should_receive(:message).with(:text, '', 'Hello') @f << "\x01\x03Hel" @f << "\x80\x02lo" end it "Ping request" do @f.should_receive(:message).with(:ping, '', 'Hello') @f << "\x89\x05Hello" end it "a pong response" do @f.should_receive(:message).with(:pong, '', 'Hello') @f << "\x8a\x05Hello" end it "256 bytes binary message in a single unmasked frame" do data = "a"*256 @f.should_receive(:message).with(:binary, '', data) @f << "\x82\x7E\x01\x00" + data end it "64KiB binary message in a single unmasked frame" do data = "a"*65536 @f.should_receive(:message).with(:binary, '', data) @f << "\x82\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data end end describe "other tests" do it "should raise a WSProtocolError if an invalid frame type is requested" do lambda { # Opcode 3 is not supported by this draft @f << "\x83\x05Hello" }.should raise_error(EventMachine::WebSocket::WSProtocolError, "Unknown opcode 3") end it "should accept a fragmented unmasked text message in 3 frames" do @f.should_receive(:message).with(:text, '', 'Hello world') @f << "\x01\x03Hel" @f << "\x00\x02lo" @f << "\x80\x06 world" end it "should raise if non-fin frame is followed by a non-continuation data frame (continuation frame would be expected)" do lambda { @f << 0b00000001 # Not fin, text @f << 0b00000001 # Length 1 @f << 'f' @f << 0b10000001 # fin, text (continutation expected) @f << 0b00000001 # Length 1 @f << 'b' }.should raise_error(EM::WebSocket::WebSocketError, 'Continuation frame expected') end it "should raise on non-fin control frames (control frames must not be fragmented)" do lambda { @f << 0b00001010 # Not fin, pong (opcode 10) @f << 0b00000000 # Length 1 }.should raise_error(EM::WebSocket::WebSocketError, 'Control frames must not be fragmented') end end end em-websocket-0.5.1/spec/unit/handshake_spec.rb0000664000000000000000000001670713326125113020010 0ustar rootrootrequire 'helper' describe EM::WebSocket::Handshake do def handshake(request, secure = false) handshake = EM::WebSocket::Handshake.new(secure) handshake.receive_data(format_request(request)) handshake end before :each do @request = { :port => 80, :method => "GET", :path => "/demo", :headers => { 'Host' => 'example.com', 'Connection' => 'Upgrade', 'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00', 'Sec-WebSocket-Protocol' => 'sample', 'Upgrade' => 'WebSocket', 'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5', 'Origin' => 'http://example.com' }, :body => '^n:ds[4U' } @secure_request = @request.merge(:port => 443) @response = { :headers => { "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Location" => "ws://example.com/demo", "Sec-WebSocket-Origin" => "http://example.com", "Sec-WebSocket-Protocol" => "sample" }, :body => "8jKS\'y:G*Co,Wxa-" } @secure_response = @response.merge(:headers => @response[:headers].merge('Sec-WebSocket-Location' => "wss://example.com/demo")) end it "should handle good request" do handshake(@request).should succeed_with_upgrade(@response) end it "should handle good request to secure default port if secure mode is enabled" do handshake(@secure_request, true). should succeed_with_upgrade(@secure_response) end it "should not handle good request to secure default port if secure mode is disabled" do handshake(@secure_request, false). should_not succeed_with_upgrade(@secure_response) end it "should handle good request on nondefault port" do @request[:port] = 8081 @request[:headers]['Host'] = 'example.com:8081' @response[:headers]['Sec-WebSocket-Location'] = 'ws://example.com:8081/demo' handshake(@request).should succeed_with_upgrade(@response) end it "should handle good request to secure nondefault port" do @secure_request[:port] = 8081 @secure_request[:headers]['Host'] = 'example.com:8081' @secure_response[:headers]['Sec-WebSocket-Location'] = 'wss://example.com:8081/demo' handshake(@secure_request, true). should succeed_with_upgrade(@secure_response) end it "should handle good request with no protocol" do @request[:headers].delete('Sec-WebSocket-Protocol') @response[:headers].delete("Sec-WebSocket-Protocol") handshake(@request).should succeed_with_upgrade(@response) end it "should handle extra headers by simply ignoring them" do @request[:headers]['EmptyValue'] = "" @request[:headers]['AKey'] = "AValue" handshake(@request).should succeed_with_upgrade(@response) end it "should raise error on HTTP request" do @request[:headers] = { 'Host' => 'www.google.com', 'User-Agent' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 GTB6 GTBA', 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language' => 'en-us,en;q=0.5', 'Accept-Encoding' => 'gzip,deflate', 'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Keep-Alive' => '300', 'Connection' => 'keep-alive', } handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError) end it "should raise error on wrong method" do @request[:method] = 'POST' handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError) end it "should raise error if upgrade header incorrect" do @request[:headers]['Upgrade'] = 'NonWebSocket' handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError) end it "should raise error if Sec-WebSocket-Protocol is empty" do @request[:headers]['Sec-WebSocket-Protocol'] = '' handshake(@request).should fail_with_error(EM::WebSocket::HandshakeError) end %w[Sec-WebSocket-Key1 Sec-WebSocket-Key2].each do |header| it "should raise error if #{header} has zero spaces" do @request[:headers][header] = 'nospaces' handshake(@request). should fail_with_error(EM::WebSocket::HandshakeError, 'Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack') end end it "should raise error if Sec-WebSocket-Key1 is missing" do @request[:headers].delete("Sec-WebSocket-Key1") # The error message isn't correct since key1 is used to heuristically # determine the protocol version in use, however this test at least checks # that the handshake does correctly fail handshake(@request). should fail_with_error(EM::WebSocket::HandshakeError, 'Extra bytes after header') end it "should raise error if Sec-WebSocket-Key2 is missing" do @request[:headers].delete("Sec-WebSocket-Key2") handshake(@request). should fail_with_error(EM::WebSocket::HandshakeError, 'WebSocket key1 or key2 is missing') end it "should raise error if spaces do not divide numbers in Sec-WebSocket-Key* " do @request[:headers]['Sec-WebSocket-Key2'] = '12998 5 Y3 1.P00' handshake(@request). should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid Key "12998 5 Y3 1.P00"') end it "should raise error if the HTTP header is empty" do handshake = EM::WebSocket::Handshake.new(false) handshake.receive_data("\r\n\r\nfoobar") handshake. should fail_with_error(EM::WebSocket::HandshakeError, 'Invalid HTTP header: Could not parse data entirely (4 != 10)') end # This might seems crazy, but very occasionally we saw multiple "Upgrade: # WebSocket" headers in the wild. RFC 4.2.1 isn't particularly clear on this # point, so for now I have decided not to accept --@mloughran it "should raise error on multiple upgrade headers" do handshake = EM::WebSocket::Handshake.new(false) # Add a duplicate upgrade header headers = format_request(@request) upgrade_header = "Upgrade: WebSocket\r\n" headers.gsub!(upgrade_header, "#{upgrade_header}#{upgrade_header}") handshake.receive_data(headers) handshake.errback { |e| e.class.should == EM::WebSocket::HandshakeError e.message.should == 'Invalid upgrade header: ["WebSocket", "WebSocket"]' } end it "should cope with requests where the header is split" do request = format_request(@request) incomplete_request = request[0...(request.length / 2)] rest = request[(request.length / 2)..-1] handshake = EM::WebSocket::Handshake.new(false) handshake.receive_data(incomplete_request) handshake.instance_variable_get(:@deferred_status).should == nil # Send the remaining header handshake.receive_data(rest) handshake(@request).should succeed_with_upgrade(@response) end it "should cope with requests where the third key is split" do request = format_request(@request) # Removes last two bytes of the third key incomplete_request = request[0..-3] rest = request[-2..-1] handshake = EM::WebSocket::Handshake.new(false) handshake.receive_data(incomplete_request) handshake.instance_variable_get(:@deferred_status).should == nil # Send the remaining third key handshake.receive_data(rest) handshake(@request).should succeed_with_upgrade(@response) end it "should fail if the request URI is invalid" do @request[:path] = "/%" handshake(@request).should \ fail_with_error(EM::WebSocket::HandshakeError, 'Invalid request URI: /%') end end em-websocket-0.5.1/spec/helper.rb0000664000000000000000000001044113326125113015335 0ustar rootroot# encoding: BINARY require 'rubygems' require 'rspec' require 'em-spec/rspec' require 'em-http' require 'em-websocket' require 'em-websocket-client' require 'integration/shared_examples' require 'integration/gte_03_examples' RSpec.configure do |c| c.mock_with :rspec end class FakeWebSocketClient < EM::Connection attr_reader :handshake_response, :packets def onopen(&blk); @onopen = blk; end def onclose(&blk); @onclose = blk; end def onerror(&blk); @onerror = blk; end def onmessage(&blk); @onmessage = blk; end def initialize @state = :new @packets = [] end def receive_data(data) # puts "RECEIVE DATA #{data}" if @state == :new @handshake_response = data @onopen.call if defined? @onopen @state = :open else @onmessage.call(data) if defined? @onmessage @packets << data end end def send(application_data) send_frame(:text, application_data) end def send_frame(type, application_data) send_data construct_frame(type, application_data) end def unbind @onclose.call if defined? @onclose end private def construct_frame(type, data) "\x00#{data}\xff" end end class Draft03FakeWebSocketClient < FakeWebSocketClient private def construct_frame(type, data) frame = "" frame << EM::WebSocket::Framing03::FRAME_TYPES[type] frame << encoded_length(data.size) frame << data end def encoded_length(length) if length <= 125 [length].pack('C') # since rsv4 is 0 elsif length < 65536 # write 2 byte length "\126#{[length].pack('n')}" else # write 8 byte length "\127#{[length >> 32, length & 0xFFFFFFFF].pack("NN")}" end end end class Draft05FakeWebSocketClient < Draft03FakeWebSocketClient private def construct_frame(type, data) frame = "" frame << "\x00\x00\x00\x00" # Mask with nothing for simplicity frame << (EM::WebSocket::Framing05::FRAME_TYPES[type] | 0b10000000) frame << encoded_length(data.size) frame << data end end class Draft07FakeWebSocketClient < Draft05FakeWebSocketClient private def construct_frame(type, data) frame = "" frame << (EM::WebSocket::Framing07::FRAME_TYPES[type] | 0b10000000) # Should probably mask the data, but I get away without bothering since # the server doesn't enforce that incoming frames are masked frame << encoded_length(data.size) frame << data end end # Wrapper around em-websocket-client class Draft75WebSocketClient def onopen(&blk); @onopen = blk; end def onclose(&blk); @onclose = blk; end def onerror(&blk); @onerror = blk; end def onmessage(&blk); @onmessage = blk; end def initialize @ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/', :version => 75, :origin => 'http://example.com') @ws.errback { |err| @onerror.call if defined? @onerror } @ws.callback { @onopen.call if defined? @onopen } @ws.stream { |msg| @onmessage.call(msg) if defined? @onmessage } @ws.disconnect { @onclose.call if defined? @onclose } end def send(message) @ws.send_msg(message) end def close_connection @ws.close_connection end end def start_server(opts = {}) EM::WebSocket.run({:host => "0.0.0.0", :port => 12345}.merge(opts)) { |ws| yield ws if block_given? } end def format_request(r) data = "#{r[:method]} #{r[:path]} HTTP/1.1\r\n" header_lines = r[:headers].map { |k,v| "#{k}: #{v}" } data << [header_lines, '', r[:body]].join("\r\n") data end def format_response(r) data = r[:protocol] || "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" header_lines = r[:headers].map { |k,v| "#{k}: #{v}" } data << [header_lines, '', r[:body]].join("\r\n") data end RSpec::Matchers.define :succeed_with_upgrade do |response| match do |actual| success = nil actual.callback { |upgrade_response, handler_klass| success = (upgrade_response.lines.sort == format_response(response).lines.sort) } success end end RSpec::Matchers.define :fail_with_error do |error_klass, error_message| match do |actual| success = nil actual.errback { |e| success = (e.class == error_klass) success &= (e.message == error_message) if error_message } success end end em-websocket-0.5.1/spec/integration/0000755000000000000000000000000013326125113016052 5ustar rootrootem-websocket-0.5.1/spec/integration/gte_03_examples.rb0000664000000000000000000000255313326125113021365 0ustar rootrootshared_examples_for "a WebSocket server drafts 3 and above" do it "should force close connections after a timeout if close handshake is not sent by the client" do em { server_onerror_fired = false server_onclose_fired = false client_got_close_handshake = false start_server(:close_timeout => 0.1) { |ws| ws.onopen { # 1: Send close handshake to client EM.next_tick { ws.close(4999, "Close message") } } ws.onerror { |e| # 3: Client should receive onerror e.class.should == EM::WebSocket::WSProtocolError e.message.should == "Close handshake un-acked after 0.1s, closing tcp connection" server_onerror_fired = true } ws.onclose { server_onclose_fired = true } } start_client { |client| client.onmessage { |msg| # 2: Client does not respond to close handshake (the fake client # doesn't understand them at all hence this is in onmessage) msg.should =~ /Close message/ if version >= 6 client_got_close_handshake = true } client.onclose { server_onerror_fired.should == true server_onclose_fired.should == true client_got_close_handshake.should == true done } } } end end em-websocket-0.5.1/spec/integration/draft06_spec.rb0000664000000000000000000000636613326125113020674 0ustar rootrootrequire 'helper' describe "draft06" do include EM::SpecHelper default_timeout 1 before :each do @request = { :port => 80, :method => "GET", :path => "/demo", :headers => { 'Host' => 'example.com', 'Upgrade' => 'websocket', 'Connection' => 'Upgrade', 'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ==', 'Sec-WebSocket-Protocol' => 'sample', 'Sec-WebSocket-Origin' => 'http://example.com', 'Sec-WebSocket-Version' => '6' } } @response = { :protocol => "HTTP/1.1 101 Switching Protocols\r\n", :headers => { "Upgrade" => "websocket", "Connection" => "Upgrade", "Sec-WebSocket-Accept" => "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", } } end def start_client client = EM.connect('0.0.0.0', 12345, Draft05FakeWebSocketClient) client.send_data(format_request(@request)) yield client if block_given? return client end it_behaves_like "a websocket server" do let(:version) { 6 } end it_behaves_like "a WebSocket server drafts 3 and above" do let(:version) { 6 } end it "should open connection" do em { start_server { |server| server.onopen { server.instance_variable_get(:@handler).class.should == EventMachine::WebSocket::Handler06 } } start_client { |client| client.onopen { client.handshake_response.lines.sort. should == format_response(@response).lines.sort done } } } end it "should accept a single-frame text message (masked)" do em { start_server { |server| server.onmessage { |msg| msg.should == 'Hello' if msg.respond_to?(:encoding) msg.encoding.should == Encoding.find("UTF-8") end done } server.onerror { fail } } start_client { |client| client.onopen { client.send_data("\x00\x00\x01\x00\x84\x05Ielln") } } } end it "should return close code and reason if closed via handshake" do em { start_server { |ws| ws.onclose { |event| # 2. Receive close event in server event.should == { :code => 4004, :reason => "close reason", :was_clean => true, } done } } start_client { |client| client.onopen { # 1: Send close handshake close_data = [4004].pack('n') close_data << "close reason" client.send_frame(:close, close_data) } } } end it "should return close code 1005 if no code was specified" do em { start_server { |ws| ws.onclose { |event| event.should == { :code => 1005, :reason => "", :was_clean => true, } done } } start_client { |client| client.onopen { client.send_frame(:close, '') } } } end it "should report that close codes are supported" do em { start_server { |ws| ws.onopen { ws.supports_close_codes?.should == true done } } start_client } end end em-websocket-0.5.1/spec/integration/shared_examples.rb0000644000000000000000000001322513326125113021546 0ustar rootroot# encoding: UTF-8 # These tests are run against all draft versions # shared_examples_for "a websocket server" do it "should expose the protocol version" do em { start_server { |ws| ws.onopen { |handshake| handshake.protocol_version.should == version done } } start_client } end it "should expose the origin header" do em { start_server { |ws| ws.onopen { |handshake| handshake.origin.should == 'http://example.com' done } } start_client } end it "should send messages successfully" do em { start_server { |ws| ws.onmessage { |message| message.should == "hello server" done } } start_client { |client| client.onopen { client.send("hello server") } } } end it "should allow connection to be closed with valid close code" do em { start_server { |ws| ws.onopen { ws.close(4004, "Bye bye") done } } start_client # TODO: Use a real client which understands how to respond to closing # handshakes, sending the handshake currently untested } end it "should raise error if if invalid close code is used" do em { start_server { |ws| ws.onopen { lambda { ws.close(2000) }.should raise_error("Application code may only use codes from 1000, 3000-4999") done } } start_client } end it "should call onclose with was_clean set to false if connection closed without closing handshake by server" do em { start_server { |ws| ws.onopen { # Close tcp connection (no close handshake) ws.close_connection } ws.onclose { |event| event.should == {:code => 1006, :was_clean => false} done } } start_client } end it "should call onclose with was_clean set to false if connection closed without closing handshake by client" do em { start_server { |ws| ws.onclose { |event| event.should == {:code => 1006, :was_clean => false} done } } start_client { |client| client.onopen { # Close tcp connection (no close handshake) client.close_connection } } } end it "should call onerror if an application error raised in onopen" do em { start_server { |ws| ws.onopen { raise "application error" } ws.onerror { |e| e.message.should == "application error" done } } start_client } end it "should call onerror if an application error raised in onmessage" do em { start_server { |server| server.onmessage { raise "application error" } server.onerror { |e| e.message.should == "application error" done } } start_client { |client| client.onopen { client.send('a message') } } } end it "should call onerror in an application error raised in onclose" do em { start_server { |server| server.onclose { raise "application error" } server.onerror { |e| e.message.should == "application error" done } } start_client { |client| client.onopen { EM.add_timer(0.1) { client.close_connection } } } } end it "should close the connection when a too long frame is sent" do em { start_server { |server| server.max_frame_size = 20 server.onerror { |e| # 3: Error should be reported to server e.class.should == EventMachine::WebSocket::WSMessageTooBigError e.message.should =~ /Frame length too long/ } } start_client { |client| client.onopen { EM.next_tick { client.send("This message is longer than 20 characters") } } client.onmessage { |msg| # 4: This is actually the close message. Really need to use a real # WebSocket client in these tests... done } client.onclose { # 4: Drafts 75 & 76 don't send a close message, they just close the # connection done } } } end # Only run these tests on ruby 1.9 if "a".respond_to?(:force_encoding) it "should raise error if you try to send non utf8 text data to ws" do em { start_server { |server| server.onopen { # Create a string which claims to be UTF-8 but which is not s = "ê" # utf-8 string s.encode!("ISO-8859-1") s.force_encoding("UTF-8") s.valid_encoding?.should == false # now invalid utf8 # Send non utf8 encoded data server.send(s) } server.onerror { |error| error.class.should == EventMachine::WebSocket::WebSocketError error.message.should == "Data sent to WebSocket must be valid UTF-8 but was UTF-8 (valid: false)" done } } start_client { } } end it "should not change the encoding of strings sent to send [antiregression]" do em { start_server { |server| server.onopen { s = "example string" s.force_encoding("UTF-8") server.send(s) s.encoding.should == Encoding.find("UTF-8") done } } start_client { } } end end end em-websocket-0.5.1/spec/integration/draft03_spec.rb0000664000000000000000000001630713326125113020665 0ustar rootrootrequire 'helper' describe "draft03" do include EM::SpecHelper default_timeout 1 before :each do @request = { :port => 80, :method => "GET", :path => "/demo", :headers => { 'Host' => 'example.com', 'Connection' => 'Upgrade', 'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00', 'Sec-WebSocket-Protocol' => 'sample', 'Upgrade' => 'WebSocket', 'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5', 'Origin' => 'http://example.com', 'Sec-WebSocket-Draft' => '3' }, :body => '^n:ds[4U' } @response = { :headers => { "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Location" => "ws://example.com/demo", "Sec-WebSocket-Origin" => "http://example.com", "Sec-WebSocket-Protocol" => "sample" }, :body => "8jKS\'y:G*Co,Wxa-" } end def start_client client = EM.connect('0.0.0.0', 12345, Draft03FakeWebSocketClient) client.send_data(format_request(@request)) yield client if block_given? return client end it_behaves_like "a websocket server" do let(:version) { 3 } end it_behaves_like "a WebSocket server drafts 3 and above" do let(:version) { 3 } end # These examples are straight from the spec # http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03#section-4.6 describe "examples from the spec" do it "should accept a single-frame text message" do em { start_server { |ws| ws.onmessage { |msg| msg.should == 'Hello' done } } start_client { |client| client.onopen { client.send_data("\x04\x05Hello") } } } end it "should accept a fragmented text message" do em { start_server { |ws| ws.onmessage { |msg| msg.should == 'Hello' done } } connection = start_client # Send frame connection.onopen { connection.send_data("\x84\x03Hel") connection.send_data("\x00\x02lo") } } end it "should accept a ping request and respond with the same body" do em { start_server connection = start_client # Send frame connection.onopen { connection.send_data("\x02\x05Hello") } connection.onmessage { |frame| next if frame.nil? frame.should == "\x03\x05Hello" done } } end it "should accept a 256 bytes binary message in a single frame" do em { data = "a" * 256 start_server { |ws| ws.onbinary { |msg| msg.encoding.should == Encoding.find("BINARY") if defined?(Encoding) msg.should == data done } } connection = start_client # Send frame connection.onopen { connection.send_data("\x05\x7E\x01\x00" + data) } } end it "should accept a 64KiB binary message in a single frame" do em { data = "a" * 65536 start_server { |ws| ws.onbinary { |msg| msg.encoding.should == Encoding.find("BINARY") if defined?(Encoding) msg.should == data done } } connection = start_client # Send frame connection.onopen { connection.send_data("\x05\x7F\x00\x00\x00\x00\x00\x01\x00\x00" + data) } } end end describe "close handling" do it "should respond to a new close frame with a close frame" do em { start_server connection = start_client # Send close frame connection.onopen { connection.send_data("\x01\x00") } # Check that close ack received connection.onmessage { |frame| frame.should == "\x01\x00" done } } end it "should close the connection on receiving a close acknowlegement and call onclose with close code 1005 and was_clean=true (initiated by server)" do em { ack_received = false start_server { |ws| ws.onopen { # 2. Send a close frame EM.next_tick { ws.close } } # 5. Onclose event on server ws.onclose { |event| event.should == { :code => 1005, :reason => "", :was_clean => true, } done } } # 1. Create a fake client which sends draft 76 handshake connection = start_client # 3. Check that close frame recieved and acknowlege it connection.onmessage { |frame| frame.should == "\x01\x00" ack_received = true connection.send_data("\x01\x00") } # 4. Check that connection is closed _after_ the ack connection.onclose { ack_received.should == true } } end # it "should repur" # it "should return close code 1005 and was_clean=true after closing handshake (initiated by client)" do em { start_server { |ws| ws.onclose { |event| event.should == { :code => 1005, :reason => "", :was_clean => true, } done } } start_client { |client| client.onopen { client.send_data("\x01\x00") } } } end it "should not allow data frame to be sent after close frame sent" do em { start_server { |ws| ws.onopen { # 2. Send a close frame EM.next_tick { ws.close } # 3. Check that exception raised if I attempt to send more data EM.add_timer(0.1) { lambda { ws.send('hello world') }.should raise_error(EM::WebSocket::WebSocketError, 'Cannot send data frame since connection is closing') done } } } # 1. Create a fake client which sends draft 76 handshake start_client } end it "should still respond to control frames after close frame sent" do em { start_server { |ws| ws.onopen { # 2. Send a close frame EM.next_tick { ws.close } } } # 1. Create a fake client which sends draft 76 handshake connection = start_client connection.onmessage { |frame| if frame == "\x01\x00" # 3. After the close frame is received send a ping frame, but # don't respond with a close ack connection.send_data("\x02\x05Hello") else # 4. Check that the pong is received frame.should == "\x03\x05Hello" done end } } end it "should report that close codes are not supported" do em { start_server { |ws| ws.onopen { ws.supports_close_codes?.should == false done } } start_client } end end end em-websocket-0.5.1/spec/integration/draft75_spec.rb0000664000000000000000000000562713326125113020701 0ustar rootrootrequire 'helper' # These integration tests are older and use a different testing style to the # integration tests for newer drafts. They use EM::HttpRequest which happens # to currently estabish a websocket connection using the draft75 protocol. # describe "WebSocket server draft75" do include EM::SpecHelper default_timeout 1 def start_client client = Draft75WebSocketClient.new yield client if block_given? return client end it_behaves_like "a websocket server" do let(:version) { 75 } end it "should automatically complete WebSocket handshake" do em { MSG = "Hello World!" EventMachine.add_timer(0.1) do ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/') ws.errback { fail } ws.callback { } ws.stream { |msg| msg.data.should == MSG EventMachine.stop } end start_server { |ws| ws.onopen { ws.send MSG } } } end it "should split multiple messages into separate callbacks" do em { messages = %w[1 2] received = [] EventMachine.add_timer(0.1) do ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/') ws.errback { fail } ws.stream {|msg|} ws.callback { ws.send_msg messages[0] ws.send_msg messages[1] } end start_server { |ws| ws.onopen {} ws.onclose {} ws.onmessage {|msg| msg.should == messages[received.size] received.push msg EventMachine.stop if received.size == messages.size } } } end it "should call onclose callback when client closes connection" do em { EventMachine.add_timer(0.1) do ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/') ws.errback { fail } ws.callback { ws.close_connection } ws.stream{|msg|} end start_server { |ws| ws.onopen {} ws.onclose { ws.state.should == :closed EventMachine.stop } } } end it "should call onerror callback with raised exception and close connection on bad handshake" do em { EventMachine.add_timer(0.1) do http = EM::HttpRequest.new('http://127.0.0.1:12345/').get http.errback { } http.callback { fail } end start_server { |ws| ws.onopen { fail } ws.onclose { EventMachine.stop } ws.onerror {|e| e.should be_an_instance_of EventMachine::WebSocket::HandshakeError e.message.should match('Not an upgrade request') EventMachine.stop } } } end it "should report that close codes are not supported" do em { start_server { |ws| ws.onopen { ws.supports_close_codes?.should == false done } } start_client } end end em-websocket-0.5.1/spec/integration/common_spec.rb0000664000000000000000000000576313326125113020716 0ustar rootrootrequire 'helper' # These tests are not specific to any particular draft of the specification # describe "WebSocket server" do include EM::SpecHelper default_timeout 1 it "should fail on non WebSocket requests" do em { EM.add_timer(0.1) do http = EM::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0 http.errback { done } http.callback { fail } end start_server } end it "should expose the WebSocket request headers, path and query params" do em { EM.add_timer(0.1) do ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/', :origin => 'http://example.com') ws.errback { fail } ws.callback { ws.close_connection } ws.stream { |msg| } end start_server do |ws| ws.onopen { |handshake| headers = handshake.headers headers["Connection"].should == "Upgrade" headers["Upgrade"].should == "websocket" headers["Host"].to_s.should == "127.0.0.1:12345" handshake.path.should == "/" handshake.query.should == {} handshake.origin.should == 'http://example.com' } ws.onclose { ws.state.should == :closed done } end } end it "should expose the WebSocket path and query params when nonempty" do em { EM.add_timer(0.1) do ws = EventMachine::WebSocketClient.connect('ws://127.0.0.1:12345/hello?foo=bar&baz=qux') ws.errback { fail } ws.callback { ws.close_connection } ws.stream { |msg| } end start_server do |ws| ws.onopen { |handshake| handshake.path.should == '/hello' handshake.query_string.split('&').sort. should == ["baz=qux", "foo=bar"] handshake.query.should == {"foo"=>"bar", "baz"=>"qux"} } ws.onclose { ws.state.should == :closed done } end } end it "should raise an exception if frame sent before handshake complete" do em { # 1. Start WebSocket server start_server { |ws| # 3. Try to send a message to the socket lambda { ws.send('early message') }.should raise_error('Cannot send data before onopen callback') done } # 2. Connect a dumb TCP connection (will not send handshake) EM.connect('0.0.0.0', 12345, EM::Connection) } end it "should allow the server to be started inside an existing EM" do em { EM.add_timer(0.1) do http = EM::HttpRequest.new('http://127.0.0.1:12345/').get :timeout => 0 http.errback { |e| done } http.callback { fail } end start_server do |ws| ws.onopen { |handshake| headers = handshake.headers headers["Host"].to_s.should == "127.0.0.1:12345" } ws.onclose { ws.state.should == :closed done } end } end end em-websocket-0.5.1/spec/integration/draft13_spec.rb0000664000000000000000000000455013326125113020663 0ustar rootroot# encoding: BINARY require 'helper' describe "draft13" do include EM::SpecHelper default_timeout 1 before :each do @request = { :port => 80, :method => "GET", :path => "/demo", :headers => { 'Host' => 'example.com', 'Upgrade' => 'websocket', 'Connection' => 'Upgrade', 'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ==', 'Sec-WebSocket-Protocol' => 'sample', 'Sec-WebSocket-Origin' => 'http://example.com', 'Sec-WebSocket-Version' => '13' } } @response = { :protocol => "HTTP/1.1 101 Switching Protocols\r\n", :headers => { "Upgrade" => "websocket", "Connection" => "Upgrade", "Sec-WebSocket-Accept" => "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", } } end def start_client client = EM.connect('0.0.0.0', 12345, Draft07FakeWebSocketClient) client.send_data(format_request(@request)) yield client if block_given? return client end it_behaves_like "a websocket server" do let(:version) { 13 } end it_behaves_like "a WebSocket server drafts 3 and above" do let(:version) { 13 } end it "should send back the correct handshake response" do em { start_server connection = start_client connection.onopen { connection.handshake_response.lines.sort. should == format_response(@response).lines.sort done } } end # TODO: This test would be much nicer with a real websocket client... it "should support sending pings and binding to onpong" do em { start_server { |ws| ws.onopen { ws.should be_pingable EM.next_tick { ws.ping('hello').should == true } } ws.onpong { |data| data.should == 'hello' done } } connection = start_client # Confusing, fake onmessage means any data after the handshake connection.onmessage { |data| # This is what a ping looks like data.should == "\x89\x05hello" # This is what a pong looks like connection.send_data("\x8a\x05hello") } } end it "should report that close codes are supported" do em { start_server { |ws| ws.onopen { ws.supports_close_codes?.should == true done } } start_client } end end em-websocket-0.5.1/spec/integration/draft05_spec.rb0000664000000000000000000000214513326125113020662 0ustar rootrootrequire 'helper' describe "draft05" do include EM::SpecHelper default_timeout 1 before :each do @request = { :port => 80, :method => "GET", :path => "/demo", :headers => { 'Host' => 'example.com', 'Upgrade' => 'websocket', 'Connection' => 'Upgrade', 'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ==', 'Sec-WebSocket-Protocol' => 'sample', 'Sec-WebSocket-Origin' => 'http://example.com', 'Sec-WebSocket-Version' => '5' } } end def start_client client = EM.connect('0.0.0.0', 12345, Draft05FakeWebSocketClient) client.send_data(format_request(@request)) yield client if block_given? return client end it_behaves_like "a websocket server" do let(:version) { 5 } end it_behaves_like "a WebSocket server drafts 3 and above" do let(:version) { 5 } end it "should report that close codes are not supported" do em { start_server { |ws| ws.onopen { ws.supports_close_codes?.should == false done } } start_client } end end em-websocket-0.5.1/spec/integration/draft76_spec.rb0000664000000000000000000001355213326125113020676 0ustar rootroot# encoding: BINARY require 'helper' describe "WebSocket server draft76" do include EM::SpecHelper default_timeout 1 before :each do @request = { :port => 80, :method => "GET", :path => "/demo", :headers => { 'Host' => 'example.com', 'Connection' => 'Upgrade', 'Sec-WebSocket-Key2' => '12998 5 Y3 1 .P00', 'Sec-WebSocket-Protocol' => 'sample', 'Upgrade' => 'WebSocket', 'Sec-WebSocket-Key1' => '4 @1 46546xW%0l 1 5', 'Origin' => 'http://example.com' }, :body => '^n:ds[4U' } @response = { :headers => { "Upgrade" => "WebSocket", "Connection" => "Upgrade", "Sec-WebSocket-Location" => "ws://example.com/demo", "Sec-WebSocket-Origin" => "http://example.com", "Sec-WebSocket-Protocol" => "sample" }, :body => "8jKS\'y:G*Co,Wxa-" } end def start_client client = EM.connect('0.0.0.0', 12345, FakeWebSocketClient) client.send_data(format_request(@request)) yield client if block_given? return client end it_behaves_like "a websocket server" do let(:version) { 76 } end it "should send back the correct handshake response" do em { start_server start_client { |connection| connection.onopen { connection.handshake_response.lines.sort. should == format_response(@response).lines.sort done } } } end it "should send closing frame back and close the connection after recieving closing frame" do em { start_server connection = start_client # Send closing frame after handshake complete connection.onopen { connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING) } # Check that this causes a termination string to be returned and the # connection close connection.onclose { connection.packets[0].should == EM::WebSocket::Handler76::TERMINATE_STRING done } } end it "should ignore any data received after the closing frame" do em { start_server { |ws| # Fail if foobar message is received ws.onmessage { |msg| fail } } connection = start_client # Send closing frame after handshake complete, followed by another msg connection.onopen { connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING) connection.send('foobar') } connection.onclose { done } } end it "should accept null bytes within the frame after a line return" do em { start_server { |ws| ws.onmessage { |msg| msg.should == "\n\000" } } connection = start_client # Send closing frame after handshake complete connection.onopen { connection.send_data("\000\n\000\377") connection.send_data(EM::WebSocket::Handler76::TERMINATE_STRING) } connection.onclose { done } } end it "should handle unreasonable frame lengths by calling onerror callback" do em { start_server { |server| server.onerror { |error| error.should be_an_instance_of EM::WebSocket::WSMessageTooBigError error.message.should == "Frame length too long (1180591620717411303296 bytes)" done } } client = start_client # This particular frame indicates a message length of # 1180591620717411303296 bytes. Such a message would previously cause # a "bignum too big to convert into `long'" error. # However it is clearly unreasonable and should be rejected. client.onopen { client.send_data("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00") } } end it "should handle impossible frames by calling onerror callback" do em { start_server { |server| server.onerror { |error| error.should be_an_instance_of EM::WebSocket::WSProtocolError error.message.should == "Invalid frame received" done } } client = start_client client.onopen { client.send_data("foobar") # Does not start with \x00 or \xff } } end it "should handle invalid http requests by raising HandshakeError passed to onerror callback" do em { start_server { |server| server.onerror { |error| error.should be_an_instance_of EM::WebSocket::HandshakeError error.message.should == "Invalid HTTP header: Could not parse data entirely (1 != 29)" done } } client = EM.connect('0.0.0.0', 12345, FakeWebSocketClient) client.send_data("This is not a HTTP header\r\n\r\n") } end it "should handle handshake request split into two TCP packets" do em { start_server # Create a fake client which sends draft 76 handshake connection = EM.connect('0.0.0.0', 12345, FakeWebSocketClient) data = format_request(@request) # Sends first half of the request connection.send_data(data[0...(data.length / 2)]) connection.onopen { connection.handshake_response.lines.sort. should == format_response(@response).lines.sort done } EM.add_timer(0.1) do # Sends second half of the request connection.send_data(data[(data.length / 2)..-1]) end } end it "should report that close codes are not supported" do em { start_server { |ws| ws.onopen { ws.supports_close_codes?.should == false done } } start_client } end it "should call onclose when the server closes the connection [antiregression]" do em { start_server { |ws| ws.onopen { EM.add_timer(0.1) { ws.close() } } ws.onclose { done } } start_client } end end em-websocket-0.5.1/Gemfile0000664000000000000000000000037013326125113014072 0ustar rootrootsource "http://rubygems.org" gemspec gem "em-websocket-client", git: "git@github.com:movitto/em-websocket-client.git", branch: "expose-websocket-api" gem "em-spec", "~> 0.2.6" gem "em-http-request", "~> 1.1.1" gem "rspec", "~> 2.12.0" gem "rake" em-websocket-0.5.1/em-websocket.gemspec0000664000000000000000000000163013326125113016531 0ustar rootroot# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "em-websocket/version" Gem::Specification.new do |s| s.name = "em-websocket" s.version = EventMachine::Websocket::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Ilya Grigorik", "Martyn Loughran"] s.email = ["ilya@igvita.com", "me@mloughran.com"] s.homepage = "http://github.com/igrigorik/em-websocket" s.summary = %q{EventMachine based WebSocket server} s.description = %q{EventMachine based WebSocket server} s.rubyforge_project = "em-websocket" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] s.add_dependency("eventmachine", ">= 0.12.9") s.add_dependency("http_parser.rb", '~> 0.6.0') end em-websocket-0.5.1/examples/0000755000000000000000000000000013326125113014413 5ustar rootrootem-websocket-0.5.1/examples/test.html0000644000000000000000000000163713326125113016267 0ustar rootroot
em-websocket-0.5.1/examples/multicast.rb0000644000000000000000000000175313326125113016753 0ustar rootrootrequire 'em-websocket' # requires the twitter-stream gem require 'twitter/json_stream' require 'json' # # broadcast all ruby related tweets to all connected users! # username = ARGV.shift password = ARGV.shift raise "need username and password" if !username or !password EventMachine.run { @channel = EM::Channel.new @twitter = Twitter::JSONStream.connect( :path => '/1/statuses/filter.json?track=ruby', :auth => "#{username}:#{password}", :ssl => true ) @twitter.each_item do |status| status = JSON.parse(status) @channel.push "#{status['user']['screen_name']}: #{status['text']}" end EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |ws| ws.onopen { sid = @channel.subscribe { |msg| ws.send msg } @channel.push "#{sid} connected!" ws.onmessage { |msg| @channel.push "<#{sid}>: #{msg}" } ws.onclose { @channel.unsubscribe(sid) } } end puts "Server started" } em-websocket-0.5.1/examples/echo.rb0000664000000000000000000000104113326125113015654 0ustar rootrootrequire File.expand_path('../../lib/em-websocket', __FILE__) EM.run { EM::WebSocket.run(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws| ws.onopen { |handshake| puts "WebSocket opened #{{ :path => handshake.path, :query => handshake.query, :origin => handshake.origin, }}" ws.send "Hello Client!" } ws.onmessage { |msg| ws.send "Pong: #{msg}" } ws.onclose { puts "WebSocket closed" } ws.onerror { |e| puts "Error: #{e.message}" } end } em-websocket-0.5.1/examples/ping.rb0000644000000000000000000000106613326125113015700 0ustar rootrootrequire File.expand_path('../../lib/em-websocket', __FILE__) EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws| timer = nil ws.onopen { puts "Ping supported: #{ws.pingable?}" timer = EM.add_periodic_timer(1) { p ["Sent ping", ws.ping('hello')] } } ws.onpong { |value| puts "Received pong: #{value}" } ws.onping { |value| puts "Received ping: #{value}" } ws.onclose { EM.cancel_timer(timer) puts "WebSocket closed" } ws.onerror { |e| puts "Error: #{e.message}" } end em-websocket-0.5.1/.gitignore0000644000000000000000000000003313326125113014561 0ustar rootroot*.gemspec pkg Gemfile.lock em-websocket-0.5.1/lib/0000755000000000000000000000000013326125113013343 5ustar rootrootem-websocket-0.5.1/lib/em-websocket.rb0000664000000000000000000000103713326125113016260 0ustar rootroot$:.unshift(File.dirname(__FILE__) + '/../lib') require "eventmachine" %w[ debugger websocket connection handshake handshake75 handshake76 handshake04 framing76 framing03 framing04 framing05 framing07 close75 close03 close05 close06 masking04 message_processor_03 message_processor_06 handler handler75 handler76 handler03 handler05 handler06 handler07 handler08 handler13 ].each do |file| require "em-websocket/#{file}" end unless ''.respond_to?(:getbyte) class String def getbyte(i) self[i] end end end em-websocket-0.5.1/lib/em-websocket/0000755000000000000000000000000013326125113015730 5ustar rootrootem-websocket-0.5.1/lib/em-websocket/handshake04.rb0000644000000000000000000000144513326125113020353 0ustar rootrootrequire 'digest/sha1' require 'base64' module EventMachine module WebSocket module Handshake04 def self.handshake(headers, _, __) # Required unless key = headers['sec-websocket-key'] raise HandshakeError, "sec-websocket-key header is required" end string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11" signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp upgrade = ["HTTP/1.1 101 Switching Protocols"] upgrade << "Upgrade: websocket" upgrade << "Connection: Upgrade" upgrade << "Sec-WebSocket-Accept: #{signature}" # TODO: Support sec-websocket-protocol # TODO: sec-websocket-extensions return upgrade.join("\r\n") + "\r\n\r\n" end end end end em-websocket-0.5.1/lib/em-websocket/handler76.rb0000664000000000000000000000043213326125113020050 0ustar rootroot# encoding: BINARY module EventMachine module WebSocket class Handler76 < Handler include Handshake76 include Framing76 include Close75 # "\377\000" is octet version and "\xff\x00" is hex version TERMINATE_STRING = "\xff\x00" end end end em-websocket-0.5.1/lib/em-websocket/handler07.rb0000644000000000000000000000024613326125113020043 0ustar rootrootmodule EventMachine module WebSocket class Handler07 < Handler include Framing07 include MessageProcessor06 include Close06 end end end em-websocket-0.5.1/lib/em-websocket/framing07.rb0000664000000000000000000001262013326125113020052 0ustar rootroot# encoding: BINARY module EventMachine module WebSocket module Framing07 def initialize_framing @data = MaskedString.new @application_data_buffer = '' # Used for MORE frames @frame_type = nil end def process_data error = false while !error && @data.size >= 2 pointer = 0 fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000 # Ignoring rsv1-3 for now opcode = @data.getbyte(pointer) & 0b00001111 pointer += 1 mask = (@data.getbyte(pointer) & 0b10000000) == 0b10000000 length = @data.getbyte(pointer) & 0b01111111 pointer += 1 # raise WebSocketError, 'Data from client must be masked' unless mask payload_length = case length when 127 # Length defined by 8 bytes # Check buffer size if @data.getbyte(pointer+8-1) == nil debug [:buffer_incomplete, @data] error = true next end # Only using the last 4 bytes for now, till I work out how to # unpack 8 bytes. I'm sure 4GB frames will do for now :) l = @data.getbytes(pointer+4, 4).unpack('N').first pointer += 8 l when 126 # Length defined by 2 bytes # Check buffer size if @data.getbyte(pointer+2-1) == nil debug [:buffer_incomplete, @data] error = true next end l = @data.getbytes(pointer, 2).unpack('n').first pointer += 2 l else length end # Compute the expected frame length frame_length = pointer + payload_length frame_length += 4 if mask if frame_length > @connection.max_frame_size raise WSMessageTooBigError, "Frame length too long (#{frame_length} bytes)" end # Check buffer size if @data.getbyte(frame_length - 1) == nil debug [:buffer_incomplete, @data] error = true next end # Remove frame header @data.slice!(0...pointer) pointer = 0 # Read application data (unmasked if required) @data.read_mask if mask pointer += 4 if mask application_data = @data.getbytes(pointer, payload_length) pointer += payload_length @data.unset_mask if mask # Throw away data up to pointer @data.slice!(0...pointer) frame_type = opcode_to_type(opcode) if frame_type == :continuation if !@frame_type raise WSProtocolError, 'Continuation frame not expected' end else # Not a continuation frame if @frame_type && data_frame?(frame_type) raise WSProtocolError, "Continuation frame expected" end end # Validate that control frames are not fragmented if !fin && !data_frame?(frame_type) raise WSProtocolError, 'Control frames must not be fragmented' end if !fin debug [:moreframe, frame_type, application_data] @application_data_buffer << application_data # The message type is passed in the first frame @frame_type ||= frame_type else # Message is complete if frame_type == :continuation @application_data_buffer << application_data message(@frame_type, '', @application_data_buffer) @application_data_buffer = '' @frame_type = nil else message(frame_type, '', application_data) end end end # end while end def send_frame(frame_type, application_data) debug [:sending_frame, frame_type, application_data] if @state == :closing && data_frame?(frame_type) raise WebSocketError, "Cannot send data frame since connection is closing" end frame = '' opcode = type_to_opcode(frame_type) byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0 frame << byte1 length = application_data.size if length <= 125 byte2 = length # since rsv4 is 0 frame << byte2 elsif length < 65536 # write 2 byte length frame << 126 frame << [length].pack('n') else # write 8 byte length frame << 127 frame << [length >> 32, length & 0xFFFFFFFF].pack("NN") end frame << application_data @connection.send_data(frame) end def send_text_frame(data) send_frame(:text, data) end private FRAME_TYPES = { :continuation => 0, :text => 1, :binary => 2, :close => 8, :ping => 9, :pong => 10, } FRAME_TYPES_INVERSE = FRAME_TYPES.invert # Frames are either data frames or control frames DATA_FRAMES = [:text, :binary, :continuation] def type_to_opcode(frame_type) FRAME_TYPES[frame_type] || raise("Unknown frame type") end def opcode_to_type(opcode) FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}") end def data_frame?(type) DATA_FRAMES.include?(type) end end end end em-websocket-0.5.1/lib/em-websocket/framing76.rb0000664000000000000000000000633613326125113020067 0ustar rootroot# encoding: BINARY module EventMachine module WebSocket module Framing76 def initialize_framing @data = '' end def process_data debug [:message, @data] # This algorithm comes straight from the spec # http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-5.3 error = false while !error return if @data.size == 0 pointer = 0 frame_type = @data.getbyte(pointer) pointer += 1 if (frame_type & 0x80) == 0x80 # If the high-order bit of the /frame type/ byte is set length = 0 loop do return false if !@data.getbyte(pointer) b = @data.getbyte(pointer) pointer += 1 b_v = b & 0x7F length = length * 128 + b_v break unless (b & 0x80) == 0x80 end if length > @connection.max_frame_size raise WSMessageTooBigError, "Frame length too long (#{length} bytes)" end if @data.getbyte(pointer+length-1) == nil debug [:buffer_incomplete, @data] # Incomplete data - leave @data to accumulate error = true else # Straight from spec - I'm sure this isn't crazy... # 6. Read /length/ bytes. # 7. Discard the read bytes. @data = @data[(pointer+length)..-1] # If the /frame type/ is 0xFF and the /length/ was 0, then close if length == 0 @connection.send_data("\xff\x00") @state = :closing @connection.close_connection_after_writing else error = true end end else # If the high-order bit of the /frame type/ byte is _not_ set if @data.getbyte(0) != 0x00 # Close the connection since this buffer can never match raise WSProtocolError, "Invalid frame received" end # Addition to the spec to protect against malicious requests if @data.size > @connection.max_frame_size raise WSMessageTooBigError, "Frame length too long (#{@data.size} bytes)" end msg = @data.slice!(/\A\x00[^\xff]*\xff/) if msg msg.gsub!(/\A\x00|\xff\z/, '') if @state == :closing debug [:ignored_message, msg] else msg.force_encoding('UTF-8') if msg.respond_to?(:force_encoding) @connection.trigger_on_message(msg) end else error = true end end end false end # frames need to start with 0x00-0x7f byte and end with # an 0xFF byte. Per spec, we can also set the first # byte to a value betweent 0x80 and 0xFF, followed by # a leading length indicator def send_text_frame(data) debug [:sending_text_frame, data] ary = ["\x00", data, "\xff"] ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) } @connection.send_data(ary.join) end end end endem-websocket-0.5.1/lib/em-websocket/message_processor_06.rb0000664000000000000000000000413713326125113022314 0ustar rootrootmodule EventMachine module WebSocket module MessageProcessor06 def message(message_type, extension_data, application_data) debug [:message_received, message_type, application_data] case message_type when :close status_code = case application_data.length when 0 # close messages MAY contain a body nil when 1 # Illegal close frame raise WSProtocolError, "Close frames with a body must contain a 2 byte status code" else application_data.slice!(0, 2).unpack('n').first end debug [:close_frame_received, status_code, application_data] @close_info = { :code => status_code || 1005, :reason => application_data, :was_clean => true, } if @state == :closing # We can close connection immediately since no more data may be # sent or received on this connection @connection.close_connection elsif @state == :connected # Acknowlege close & echo status back to client # The connection is considered closed close_data = [status_code || 1000].pack('n') send_frame(:close, close_data) @connection.close_connection_after_writing end when :ping # Pong back the same data send_frame(:pong, application_data) @connection.trigger_on_ping(application_data) when :pong @connection.trigger_on_pong(application_data) when :text if application_data.respond_to?(:force_encoding) application_data.force_encoding("UTF-8") unless application_data.valid_encoding? raise InvalidDataError, "Invalid UTF8 data" end end @connection.trigger_on_message(application_data) when :binary @connection.trigger_on_binary(application_data) end end # Ping & Pong supported def pingable? true end end end end em-websocket-0.5.1/lib/em-websocket/close75.rb0000644000000000000000000000033413326125113017536 0ustar rootrootmodule EventMachine module WebSocket module Close75 def close_websocket(code, body) @connection.close_connection_after_writing end def supports_close_codes?; false; end end end end em-websocket-0.5.1/lib/em-websocket/debugger.rb0000644000000000000000000000032413326125113020040 0ustar rootrootmodule EventMachine module WebSocket module Debugger private def debug(*data) if @debug require 'pp' pp data puts end end end end endem-websocket-0.5.1/lib/em-websocket/handler03.rb0000664000000000000000000000024613326125113020041 0ustar rootrootmodule EventMachine module WebSocket class Handler03 < Handler include Framing03 include MessageProcessor03 include Close03 end end end em-websocket-0.5.1/lib/em-websocket/framing03.rb0000664000000000000000000001106513326125113020050 0ustar rootroot# encoding: BINARY module EventMachine module WebSocket module Framing03 def initialize_framing @data = '' @application_data_buffer = '' # Used for MORE frames @frame_type = nil end def process_data error = false while !error && @data.size > 1 pointer = 0 more = ((@data.getbyte(pointer) & 0b10000000) == 0b10000000) ^ fin # Ignoring rsv1-3 for now opcode = @data.getbyte(0) & 0b00001111 pointer += 1 # Ignoring rsv4 length = @data.getbyte(pointer) & 0b01111111 pointer += 1 payload_length = case length when 127 # Length defined by 8 bytes # Check buffer size if @data.getbyte(pointer+8-1) == nil debug [:buffer_incomplete, @data] error = true next end # Only using the last 4 bytes for now, till I work out how to # unpack 8 bytes. I'm sure 4GB frames will do for now :) l = @data[(pointer+4)..(pointer+7)].unpack('N').first pointer += 8 l when 126 # Length defined by 2 bytes # Check buffer size if @data.getbyte(pointer+2-1) == nil debug [:buffer_incomplete, @data] error = true next end l = @data[pointer..(pointer+1)].unpack('n').first pointer += 2 l else length end if payload_length > @connection.max_frame_size raise WSMessageTooBigError, "Frame length too long (#{payload_length} bytes)" end # Check buffer size if @data.getbyte(pointer+payload_length-1) == nil debug [:buffer_incomplete, @data] error = true next end # Throw away data up to pointer @data.slice!(0...pointer) # Read application data application_data = @data.slice!(0...payload_length) frame_type = opcode_to_type(opcode) if frame_type == :continuation && !@frame_type raise WSProtocolError, 'Continuation frame not expected' end if more debug [:moreframe, frame_type, application_data] @application_data_buffer << application_data # The message type is passed in the first frame @frame_type ||= frame_type else # Message is complete if frame_type == :continuation @application_data_buffer << application_data message(@frame_type, '', @application_data_buffer) @application_data_buffer = '' @frame_type = nil else message(frame_type, '', application_data) end end end # end while end def send_frame(frame_type, application_data) debug [:sending_frame, frame_type, application_data] if @state == :closing && data_frame?(frame_type) raise WebSocketError, "Cannot send data frame since connection is closing" end frame = '' opcode = type_to_opcode(frame_type) byte1 = opcode # since more, rsv1-3 are 0 frame << byte1 length = application_data.size if length <= 125 byte2 = length # since rsv4 is 0 frame << byte2 elsif length < 65536 # write 2 byte length frame << 126 frame << [length].pack('n') else # write 8 byte length frame << 127 frame << [length >> 32, length & 0xFFFFFFFF].pack("NN") end frame << application_data @connection.send_data(frame) end def send_text_frame(data) send_frame(:text, data) end private # This allows flipping the more bit to fin for draft 04 def fin; false; end FRAME_TYPES = { :continuation => 0, :close => 1, :ping => 2, :pong => 3, :text => 4, :binary => 5 } FRAME_TYPES_INVERSE = FRAME_TYPES.invert # Frames are either data frames or control frames DATA_FRAMES = [:text, :binary, :continuation] def type_to_opcode(frame_type) FRAME_TYPES[frame_type] || raise("Unknown frame type") end def opcode_to_type(opcode) FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}") end def data_frame?(type) DATA_FRAMES.include?(type) end end end end em-websocket-0.5.1/lib/em-websocket/version.rb0000664000000000000000000000010713326125113017742 0ustar rootrootmodule EventMachine module Websocket VERSION = "0.5.1" end end em-websocket-0.5.1/lib/em-websocket/handler05.rb0000664000000000000000000000024613326125113020043 0ustar rootrootmodule EventMachine module WebSocket class Handler05 < Handler include Framing05 include MessageProcessor03 include Close05 end end end em-websocket-0.5.1/lib/em-websocket/handler75.rb0000644000000000000000000000023713326125113020050 0ustar rootrootmodule EventMachine module WebSocket class Handler75 < Handler include Handshake75 include Framing76 include Close75 end end end em-websocket-0.5.1/lib/em-websocket/handshake76.rb0000664000000000000000000000432213326125113020363 0ustar rootrootrequire 'digest/md5' module EventMachine::WebSocket module Handshake76 class << self def handshake(headers, path, secure) challenge_response = solve_challenge( headers['sec-websocket-key1'], headers['sec-websocket-key2'], headers['third-key'] ) scheme = (secure ? "wss" : "ws") location = "#{scheme}://#{headers['host']}#{path}" upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" upgrade << "Upgrade: WebSocket\r\n" upgrade << "Connection: Upgrade\r\n" upgrade << "Sec-WebSocket-Location: #{location}\r\n" upgrade << "Sec-WebSocket-Origin: #{headers['origin']}\r\n" if protocol = headers['sec-websocket-protocol'] validate_protocol!(protocol) upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n" end upgrade << "\r\n" upgrade << challenge_response return upgrade end private def solve_challenge(first, second, third) # Refer to 5.2 4-9 of the draft 76 sum = [numbers_over_spaces(first)].pack("N*") + [numbers_over_spaces(second)].pack("N*") + third Digest::MD5.digest(sum) end def numbers_over_spaces(string) unless string raise HandshakeError, "WebSocket key1 or key2 is missing" end numbers = string.scan(/[0-9]/).join.to_i spaces = string.scan(/ /).size # As per 5.2.5, abort the connection if spaces are zero. raise HandshakeError, "Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack" if spaces == 0 # As per 5.2.6, abort if numbers is not an integral multiple of spaces if numbers % spaces != 0 raise HandshakeError, "Invalid Key #{string.inspect}" end quotient = numbers / spaces if quotient > 2**32-1 raise HandshakeError, "Challenge computation out of range for key #{string.inspect}" end return quotient end def validate_protocol!(protocol) raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty? # TODO: Validate characters end end end end em-websocket-0.5.1/lib/em-websocket/framing04.rb0000664000000000000000000000045213326125113020047 0ustar rootroot# encoding: BINARY module EventMachine module WebSocket # The only difference between draft 03 framing and draft 04 framing is # that the MORE bit has been changed to a FIN bit module Framing04 include Framing03 private def fin; true; end end end endem-websocket-0.5.1/lib/em-websocket/handler06.rb0000664000000000000000000000024613326125113020044 0ustar rootrootmodule EventMachine module WebSocket class Handler06 < Handler include Framing05 include MessageProcessor06 include Close06 end end end em-websocket-0.5.1/lib/em-websocket/masking04.rb0000644000000000000000000000201113326125113020044 0ustar rootrootmodule EventMachine module WebSocket class MaskedString < String # Read a 4 bit XOR mask - further requested bytes will be unmasked def read_mask if respond_to?(:encoding) && encoding.name != "ASCII-8BIT" raise "MaskedString only operates on BINARY strings" end raise "Too short" if bytesize < 4 # TODO - change @masking_key = String.new(self[0..3]) end # Removes the mask, behaves like a normal string again def unset_mask @masking_key = nil end def getbyte(index) if defined?(@masking_key) && @masking_key masked_char = super masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil else super end end def getbytes(start_index, count) data = '' data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) count.times do |i| data << getbyte(start_index + i) end data end end end end em-websocket-0.5.1/lib/em-websocket/websocket.rb0000664000000000000000000000271013326125113020245 0ustar rootrootmodule EventMachine module WebSocket class << self attr_accessor :max_frame_size attr_accessor :close_timeout end @max_frame_size = 10 * 1024 * 1024 # 10MB # Connections are given 60s to close after being sent a close handshake @close_timeout = 60 # All errors raised by em-websocket should descend from this class class WebSocketError < RuntimeError; end # Used for errors that occur during WebSocket handshake class HandshakeError < WebSocketError; end # Used for errors which should cause the connection to close. # See RFC6455 §7.4.1 for a full description of meanings class WSProtocolError < WebSocketError def code; 1002; end end class InvalidDataError < WSProtocolError def code; 1007; end end # 1009: Message too big to process class WSMessageTooBigError < WSProtocolError def code; 1009; end end # Start WebSocket server, including starting eventmachine run loop def self.start(options, &blk) EM.epoll EM.run { trap("TERM") { stop } trap("INT") { stop } run(options, &blk) } end # Start WebSocket server inside eventmachine run loop def self.run(options) host, port = options.values_at(:host, :port) EM.start_server(host, port, Connection, options) do |c| yield c end end def self.stop puts "Terminating WebSocket Server" EM.stop end end end em-websocket-0.5.1/lib/em-websocket/handler13.rb0000644000000000000000000000024613326125113020040 0ustar rootrootmodule EventMachine module WebSocket class Handler13 < Handler include Framing07 include MessageProcessor06 include Close06 end end end em-websocket-0.5.1/lib/em-websocket/close06.rb0000664000000000000000000000063613326125113017537 0ustar rootrootmodule EventMachine module WebSocket module Close06 def close_websocket(code, body) if code close_data = [code].pack('n') close_data << body if body send_frame(:close, close_data) else send_frame(:close, '') end @state = :closing start_close_timeout end def supports_close_codes?; true; end end end end em-websocket-0.5.1/lib/em-websocket/close05.rb0000664000000000000000000000051213326125113017527 0ustar rootrootmodule EventMachine module WebSocket module Close05 def close_websocket(code, body) # TODO: Ideally send body data and check that it matches in ack send_frame(:close, "\x53") @state = :closing start_close_timeout end def supports_close_codes?; false; end end end end em-websocket-0.5.1/lib/em-websocket/connection.rb0000664000000000000000000002367313326125113020431 0ustar rootrootmodule EventMachine module WebSocket class Connection < EventMachine::Connection include Debugger attr_writer :max_frame_size # define WebSocket callbacks def onopen(&blk); @onopen = blk; end def onclose(&blk); @onclose = blk; end def onerror(&blk); @onerror = blk; end def onmessage(&blk); @onmessage = blk; end def onbinary(&blk); @onbinary = blk; end def onping(&blk); @onping = blk; end def onpong(&blk); @onpong = blk; end def trigger_on_message(msg) @onmessage.call(msg) if defined? @onmessage end def trigger_on_binary(msg) @onbinary.call(msg) if defined? @onbinary end def trigger_on_open(handshake) @onopen.call(handshake) if defined? @onopen end def trigger_on_close(event = {}) @onclose.call(event) if defined? @onclose end def trigger_on_ping(data) @onping.call(data) if defined? @onping end def trigger_on_pong(data) @onpong.call(data) if defined? @onpong end def trigger_on_error(reason) return false unless defined? @onerror @onerror.call(reason) true end def initialize(options) @options = options @debug = options[:debug] || false @secure = options[:secure] || false @secure_proxy = options[:secure_proxy] || false @tls_options = options[:tls_options] || {} @close_timeout = options[:close_timeout] @handler = nil debug [:initialize] end # Use this method to close the websocket connection cleanly # This sends a close frame and waits for acknowlegement before closing # the connection def close(code = nil, body = nil) if code && !acceptable_close_code?(code) raise "Application code may only use codes from 1000, 3000-4999" end close_websocket_private(code, body) end # Deprecated, to be removed in version 0.6 alias :close_websocket :close def post_init start_tls(@tls_options) if @secure end def receive_data(data) debug [:receive_data, data] if @handler @handler.receive_data(data) else dispatch(data) end rescue => e debug [:error, e] # There is no code defined for application errors, so use 3000 # (which is reserved for frameworks) close_websocket_private(3000, "Application error") # These are application errors - raise unless onerror defined trigger_on_error(e) || raise(e) end def unbind debug [:unbind, :connection] @handler.unbind if @handler rescue => e debug [:error, e] # These are application errors - raise unless onerror defined trigger_on_error(e) || raise(e) end def dispatch(data) if data.match(/\A/) send_flash_cross_domain_file else @handshake ||= begin handshake = Handshake.new(@secure || @secure_proxy) handshake.callback { |upgrade_response, handler_klass| debug [:accepting_ws_version, handshake.protocol_version] debug [:upgrade_response, upgrade_response] self.send_data(upgrade_response) @handler = handler_klass.new(self, @debug) @handshake = nil trigger_on_open(handshake) } handshake.errback { |e| debug [:error, e] trigger_on_error(e) # Handshake errors require the connection to be aborted abort } handshake end @handshake.receive_data(data) end end def send_flash_cross_domain_file file = '' debug [:cross_domain, file] send_data file # handle the cross-domain request transparently # no need to notify the user about this connection @onclose = nil close_connection_after_writing end # Cache encodings since it's moderately expensive to look them up each time ENCODING_SUPPORTED = "string".respond_to?(:force_encoding) UTF8 = Encoding.find("UTF-8") if ENCODING_SUPPORTED BINARY = Encoding.find("BINARY") if ENCODING_SUPPORTED # Send a WebSocket text frame. # # A WebSocketError may be raised if the connection is in an opening or a # closing state, or if the passed in data is not valid UTF-8 # def send_text(data) # If we're using Ruby 1.9, be pedantic about encodings if ENCODING_SUPPORTED # Also accept ascii only data in other encodings for convenience unless (data.encoding == UTF8 && data.valid_encoding?) || data.ascii_only? raise WebSocketError, "Data sent to WebSocket must be valid UTF-8 but was #{data.encoding} (valid: #{data.valid_encoding?})" end # This labels the encoding as binary so that it can be combined with # the BINARY framing data.force_encoding(BINARY) else # TODO: Check that data is valid UTF-8 end if @handler @handler.send_text_frame(data) else raise WebSocketError, "Cannot send data before onopen callback" end # Revert data back to the original encoding (which we assume is UTF-8) # Doing this to avoid duping the string - there may be a better way data.force_encoding(UTF8) if ENCODING_SUPPORTED return nil end alias :send :send_text # Send a WebSocket binary frame. # def send_binary(data) if @handler @handler.send_frame(:binary, data) else raise WebSocketError, "Cannot send binary before onopen callback" end end # Send a ping to the client. The client must respond with a pong. # # In the case that the client is running a WebSocket draft < 01, false # is returned since ping & pong are not supported # def ping(body = '') if @handler @handler.pingable? ? @handler.send_frame(:ping, body) && true : false else raise WebSocketError, "Cannot ping before onopen callback" end end # Send an unsolicited pong message, as allowed by the protocol. The # client is not expected to respond to this message. # # em-websocket automatically takes care of sending pong replies to # incoming ping messages, as the protocol demands. # def pong(body = '') if @handler @handler.pingable? ? @handler.send_frame(:pong, body) && true : false else raise WebSocketError, "Cannot ping before onopen callback" end end # Test whether the connection is pingable (i.e. the WebSocket draft in # use is >= 01) def pingable? if @handler @handler.pingable? else raise WebSocketError, "Cannot test whether pingable before onopen callback" end end def supports_close_codes? if @handler @handler.supports_close_codes? else raise WebSocketError, "Cannot test before onopen callback" end end def state @handler ? @handler.state : :handshake end # Returns the maximum frame size which this connection is configured to # accept. This can be set globally or on a per connection basis, and # defaults to a value of 10MB if not set. # # The behaviour when a too large frame is received varies by protocol, # but in the newest protocols the connection will be closed with the # correct close code (1009) immediately after receiving the frame header # def max_frame_size defined?(@max_frame_size) ? @max_frame_size : WebSocket.max_frame_size end def close_timeout @close_timeout || WebSocket.close_timeout end private # As definited in draft 06 7.2.2, some failures require that the server # abort the websocket connection rather than close cleanly def abort close_connection end def close_websocket_private(code, body) if @handler debug [:closing, code] @handler.close_websocket(code, body) else # The handshake hasn't completed - should be safe to terminate abort end end # Allow applications to close with 1000, 1003, 1008, 1011, 3xxx or 4xxx. # # em-websocket uses a few other codes internally which should not be # used by applications # # Browsers generally allow connections to be closed with code 1000, # 3xxx, and 4xxx. em-websocket allows closing with a few other codes # which seem reasonable (for discussion see # https://github.com/igrigorik/em-websocket/issues/98) # # Usage from the rfc: # # 1000 indicates a normal closure # # 1003 indicates that an endpoint is terminating the connection # because it has received a type of data it cannot accept # # 1008 indicates that an endpoint is terminating the connection because # it has received a message that violates its policy # # 1011 indicates that a server is terminating the connection because it # encountered an unexpected condition that prevented it from fulfilling # the request # # Status codes in the range 3000-3999 are reserved for use by libraries, # frameworks, and applications # # Status codes in the range 4000-4999 are reserved for private use and # thus can't be registered # def acceptable_close_code?(code) case code when 1000, 1003, 1008, 1011, (3000..4999) true else false end end end end end em-websocket-0.5.1/lib/em-websocket/handler08.rb0000644000000000000000000000024613326125113020044 0ustar rootrootmodule EventMachine module WebSocket class Handler08 < Handler include Framing07 include MessageProcessor06 include Close06 end end end em-websocket-0.5.1/lib/em-websocket/handler.rb0000664000000000000000000000464313326125113017703 0ustar rootrootmodule EventMachine module WebSocket class Handler def self.klass_factory(version) case version when 75 Handler75 when 76 Handler76 when 1..3 # We'll use handler03 - I believe they're all compatible Handler03 when 5 Handler05 when 6 Handler06 when 7 Handler07 when 8 # drafts 9, 10, 11 and 12 should never change the version # number as they are all the same as version 08. Handler08 when 13 # drafts 13 to 17 all identify as version 13 as they are # only minor changes or text changes. Handler13 else # According to spec should abort the connection raise HandshakeError, "Protocol version #{version} not supported" end end include Debugger attr_reader :request, :state def initialize(connection, debug = false) @connection = connection @debug = debug @state = :connected @close_timer = nil initialize_framing end def receive_data(data) @data << data process_data rescue WSProtocolError => e fail_websocket(e) end def close_websocket(code, body) # Implemented in subclass end # Used to avoid un-acked and unclosed remaining open indefinitely def start_close_timeout @close_timer = EM::Timer.new(@connection.close_timeout) { @connection.close_connection e = WSProtocolError.new("Close handshake un-acked after #{@connection.close_timeout}s, closing tcp connection") @connection.trigger_on_error(e) } end # This corresponds to "Fail the WebSocket Connection" in the spec. def fail_websocket(e) debug [:error, e] close_websocket(e.code, e.message) @connection.close_connection_after_writing @connection.trigger_on_error(e) end def unbind @state = :closed @close_timer.cancel if @close_timer @close_info = defined?(@close_info) ? @close_info : { :code => 1006, :was_clean => false, } @connection.trigger_on_close(@close_info) end def ping # Overridden in subclass false end def pingable? # Also Overridden false end end end end em-websocket-0.5.1/lib/em-websocket/message_processor_03.rb0000664000000000000000000000265013326125113022307 0ustar rootroot# encoding: BINARY module EventMachine module WebSocket module MessageProcessor03 def message(message_type, extension_data, application_data) case message_type when :close @close_info = { :code => 1005, :reason => "", :was_clean => true, } if @state == :closing # TODO: Check that message body matches sent data # We can close connection immediately since there is no more data # is allowed to be sent or received on this connection @connection.close_connection else # Acknowlege close # The connection is considered closed send_frame(:close, application_data) @connection.close_connection_after_writing end when :ping # Pong back the same data send_frame(:pong, application_data) @connection.trigger_on_ping(application_data) when :pong @connection.trigger_on_pong(application_data) when :text if application_data.respond_to?(:force_encoding) application_data.force_encoding("UTF-8") end @connection.trigger_on_message(application_data) when :binary @connection.trigger_on_binary(application_data) end end # Ping & Pong supported def pingable? true end end end end em-websocket-0.5.1/lib/em-websocket/close03.rb0000664000000000000000000000050613326125113017530 0ustar rootrootmodule EventMachine module WebSocket module Close03 def close_websocket(code, body) # TODO: Ideally send body data and check that it matches in ack send_frame(:close, '') @state = :closing start_close_timeout end def supports_close_codes?; false; end end end end em-websocket-0.5.1/lib/em-websocket/handshake75.rb0000644000000000000000000000104113326125113020353 0ustar rootrootmodule EventMachine module WebSocket module Handshake75 def self.handshake(headers, path, secure) scheme = (secure ? "wss" : "ws") location = "#{scheme}://#{headers['host']}#{path}" upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" upgrade << "Upgrade: WebSocket\r\n" upgrade << "Connection: Upgrade\r\n" upgrade << "WebSocket-Origin: #{headers['origin']}\r\n" upgrade << "WebSocket-Location: #{location}\r\n\r\n" return upgrade end end end end em-websocket-0.5.1/lib/em-websocket/framing05.rb0000664000000000000000000001112413326125113020046 0ustar rootroot# encoding: BINARY module EventMachine module WebSocket module Framing05 def initialize_framing @data = MaskedString.new @application_data_buffer = '' # Used for MORE frames @frame_type = nil end def process_data error = false while !error && @data.size > 5 # mask plus first byte present pointer = 0 @data.read_mask pointer += 4 fin = (@data.getbyte(pointer) & 0b10000000) == 0b10000000 # Ignoring rsv1-3 for now opcode = @data.getbyte(pointer) & 0b00001111 pointer += 1 # Ignoring rsv4 length = @data.getbyte(pointer) & 0b01111111 pointer += 1 payload_length = case length when 127 # Length defined by 8 bytes # Check buffer size if @data.getbyte(pointer+8-1) == nil debug [:buffer_incomplete, @data] error = true next end # Only using the last 4 bytes for now, till I work out how to # unpack 8 bytes. I'm sure 4GB frames will do for now :) l = @data.getbytes(pointer+4, 4).unpack('N').first pointer += 8 l when 126 # Length defined by 2 bytes # Check buffer size if @data.getbyte(pointer+2-1) == nil debug [:buffer_incomplete, @data] error = true next end l = @data.getbytes(pointer, 2).unpack('n').first pointer += 2 l else length end if payload_length > @connection.max_frame_size raise WSMessageTooBigError, "Frame length too long (#{payload_length} bytes)" end # Check buffer size if @data.getbyte(pointer+payload_length-1) == nil debug [:buffer_incomplete, @data] error = true next end # Read application data application_data = @data.getbytes(pointer, payload_length) pointer += payload_length # Throw away data up to pointer @data.unset_mask @data.slice!(0...pointer) frame_type = opcode_to_type(opcode) if frame_type == :continuation && !@frame_type raise WSProtocolError, 'Continuation frame not expected' end if !fin debug [:moreframe, frame_type, application_data] @application_data_buffer << application_data @frame_type = frame_type else # Message is complete if frame_type == :continuation @application_data_buffer << application_data message(@frame_type, '', @application_data_buffer) @application_data_buffer = '' @frame_type = nil else message(frame_type, '', application_data) end end end # end while end def send_frame(frame_type, application_data) debug [:sending_frame, frame_type, application_data] if @state == :closing && data_frame?(frame_type) raise WebSocketError, "Cannot send data frame since connection is closing" end frame = '' opcode = type_to_opcode(frame_type) byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0 frame << byte1 length = application_data.size if length <= 125 byte2 = length # since rsv4 is 0 frame << byte2 elsif length < 65536 # write 2 byte length frame << 126 frame << [length].pack('n') else # write 8 byte length frame << 127 frame << [length >> 32, length & 0xFFFFFFFF].pack("NN") end frame << application_data @connection.send_data(frame) end def send_text_frame(data) send_frame(:text, data) end private FRAME_TYPES = { :continuation => 0, :close => 1, :ping => 2, :pong => 3, :text => 4, :binary => 5 } FRAME_TYPES_INVERSE = FRAME_TYPES.invert # Frames are either data frames or control frames DATA_FRAMES = [:text, :binary, :continuation] def type_to_opcode(frame_type) FRAME_TYPES[frame_type] || raise("Unknown frame type") end def opcode_to_type(opcode) FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}") end def data_frame?(type) DATA_FRAMES.include?(type) end end end end em-websocket-0.5.1/lib/em-websocket/handshake.rb0000664000000000000000000001047213326125113020211 0ustar rootrootrequire "http/parser" require "uri" module EventMachine module WebSocket # Resposible for creating the server handshake response class Handshake include EM::Deferrable attr_reader :parser, :protocol_version # Unfortunately drafts 75 & 76 require knowledge of whether the # connection is being terminated as ws/wss in order to generate the # correct handshake response def initialize(secure) @parser = Http::Parser.new @secure = secure @parser.on_headers_complete = proc { |headers| @headers = Hash[headers.map { |k,v| [k.downcase, v] }] } end def receive_data(data) @parser << data if defined? @headers process(@headers, @parser.upgrade_data) end rescue HTTP::Parser::Error => e fail(HandshakeError.new("Invalid HTTP header: #{e.message}")) end # Returns the WebSocket upgrade headers as a hash. # # Keys are strings, unmodified from the request. # def headers @parser.headers end # The same as headers, except that the hash keys are downcased # def headers_downcased @headers end # Returns the request path (excluding any query params) # def path @path end # Returns the query params as a string foo=bar&baz=... def query_string @query_string end def query Hash[query_string.split('&').map { |c| c.split('=', 2) }] end # Returns the WebSocket origin header if provided # def origin @headers["origin"] || @headers["sec-websocket-origin"] || nil end def secure? @secure end private def process(headers, remains) unless @parser.http_method == "GET" raise HandshakeError, "Must be GET request" end # Validate request path # # According to http://tools.ietf.org/search/rfc2616#section-5.1.2, an # invalid Request-URI should result in a 400 status code, but # HandshakeError's currently result in a WebSocket abort. It's not # clear which should take precedence, but an abort will do just fine. begin uri = URI.parse(@parser.request_url) @path = uri.path @query_string = uri.query || "" rescue URI::InvalidURIError raise HandshakeError, "Invalid request URI: #{@parser.request_url}" end # Validate Upgrade unless @parser.upgrade? raise HandshakeError, "Not an upgrade request" end upgrade = @headers['upgrade'] unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket' raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}" end # Determine version heuristically version = if @headers['sec-websocket-version'] # Used from drafts 04 onwards @headers['sec-websocket-version'].to_i elsif @headers['sec-websocket-draft'] # Used in drafts 01 - 03 @headers['sec-websocket-draft'].to_i elsif @headers['sec-websocket-key1'] 76 else 75 end # Additional handling of bytes after the header if required case version when 75 if !remains.empty? raise HandshakeError, "Extra bytes after header" end when 76, 1..3 if remains.length < 8 # The whole third-key has not been received yet. return nil elsif remains.length > 8 raise HandshakeError, "Extra bytes after third key" end @headers['third-key'] = remains end handshake_klass = case version when 75 Handshake75 when 76, 1..3 Handshake76 when 5, 6, 7, 8, 13 Handshake04 else # According to spec should abort the connection raise HandshakeError, "Protocol version #{version} not supported" end upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure) handler_klass = Handler.klass_factory(version) @protocol_version = version succeed(upgrade_response, handler_klass) rescue HandshakeError => e fail(e) end end end end