sonixlabs-em-websocket 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG.rdoc +80 -0
  3. data/Gemfile +3 -0
  4. data/README.md +98 -0
  5. data/Rakefile +11 -0
  6. data/em-websocket.gemspec +27 -0
  7. data/examples/echo.rb +8 -0
  8. data/examples/flash_policy_file_server.rb +21 -0
  9. data/examples/js/FABridge.js +604 -0
  10. data/examples/js/WebSocketMain.swf +0 -0
  11. data/examples/js/swfobject.js +4 -0
  12. data/examples/js/web_socket.js +312 -0
  13. data/examples/multicast.rb +47 -0
  14. data/examples/test.html +30 -0
  15. data/lib/em-websocket/client_connection.rb +19 -0
  16. data/lib/em-websocket/close03.rb +11 -0
  17. data/lib/em-websocket/close05.rb +11 -0
  18. data/lib/em-websocket/close06.rb +16 -0
  19. data/lib/em-websocket/close75.rb +10 -0
  20. data/lib/em-websocket/connection.rb +184 -0
  21. data/lib/em-websocket/debugger.rb +17 -0
  22. data/lib/em-websocket/framing03.rb +167 -0
  23. data/lib/em-websocket/framing04.rb +15 -0
  24. data/lib/em-websocket/framing05.rb +168 -0
  25. data/lib/em-websocket/framing07.rb +180 -0
  26. data/lib/em-websocket/framing76.rb +114 -0
  27. data/lib/em-websocket/handler.rb +56 -0
  28. data/lib/em-websocket/handler03.rb +10 -0
  29. data/lib/em-websocket/handler05.rb +10 -0
  30. data/lib/em-websocket/handler06.rb +10 -0
  31. data/lib/em-websocket/handler07.rb +10 -0
  32. data/lib/em-websocket/handler08.rb +10 -0
  33. data/lib/em-websocket/handler13.rb +10 -0
  34. data/lib/em-websocket/handler75.rb +9 -0
  35. data/lib/em-websocket/handler76.rb +12 -0
  36. data/lib/em-websocket/handler_factory.rb +107 -0
  37. data/lib/em-websocket/handshake04.rb +75 -0
  38. data/lib/em-websocket/handshake75.rb +21 -0
  39. data/lib/em-websocket/handshake76.rb +71 -0
  40. data/lib/em-websocket/masking04.rb +63 -0
  41. data/lib/em-websocket/message_processor_03.rb +38 -0
  42. data/lib/em-websocket/message_processor_06.rb +52 -0
  43. data/lib/em-websocket/version.rb +5 -0
  44. data/lib/em-websocket/websocket.rb +45 -0
  45. data/lib/em-websocket.rb +23 -0
  46. data/lib/sonixlabs-em-websocket.rb +1 -0
  47. data/spec/helper.rb +146 -0
  48. data/spec/integration/client_examples.rb +48 -0
  49. data/spec/integration/common_spec.rb +118 -0
  50. data/spec/integration/draft03_spec.rb +270 -0
  51. data/spec/integration/draft05_spec.rb +48 -0
  52. data/spec/integration/draft06_spec.rb +88 -0
  53. data/spec/integration/draft13_spec.rb +75 -0
  54. data/spec/integration/draft75_spec.rb +117 -0
  55. data/spec/integration/draft76_spec.rb +230 -0
  56. data/spec/integration/shared_examples.rb +91 -0
  57. data/spec/unit/framing_spec.rb +325 -0
  58. data/spec/unit/handler_spec.rb +147 -0
  59. data/spec/unit/masking_spec.rb +27 -0
  60. data/spec/unit/message_processor_spec.rb +36 -0
  61. metadata +198 -0
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler05 < Handler
4
+ include Handshake04
5
+ include Framing05
6
+ include MessageProcessor03
7
+ include Close05
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler06 < Handler
4
+ include Handshake04
5
+ include Framing05
6
+ include MessageProcessor06
7
+ include Close06
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler07 < Handler
4
+ include Handshake04
5
+ include Framing07
6
+ include MessageProcessor06
7
+ include Close06
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler08 < Handler
4
+ include Handshake04
5
+ include Framing07
6
+ include MessageProcessor06
7
+ include Close06
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler13 < Handler
4
+ include Handshake04
5
+ include Framing07
6
+ include MessageProcessor06
7
+ include Close06
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler75 < Handler
4
+ include Handshake75
5
+ include Framing76
6
+ include Close75
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler76 < Handler
4
+ include Handshake76
5
+ include Framing76
6
+ include Close75
7
+
8
+ # "\377\000" is octet version and "\xff\x00" is hex version
9
+ TERMINATE_STRING = "\xff\x00"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,107 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class HandlerFactory
4
+ PATH = /^(\w+) (\/[^\s]*) HTTP\/1\.1$/
5
+ HEADER = /^([^:]+):\s*(.+)$/
6
+
7
+ def self.build(connection, data, secure = false, debug = false)
8
+ (header, remains) = data.split("\r\n\r\n", 2)
9
+ unless remains
10
+ # The whole header has not been received yet.
11
+ return nil
12
+ end
13
+
14
+ request = {}
15
+
16
+ lines = header.split("\r\n")
17
+
18
+ # extract request path
19
+ first_line = lines.shift.match(PATH)
20
+ raise HandshakeError, "Invalid HTTP header" unless first_line
21
+ request['method'] = first_line[1].strip
22
+ request['path'] = first_line[2].strip
23
+
24
+ unless request["method"] == "GET"
25
+ raise HandshakeError, "Must be GET request"
26
+ end
27
+
28
+ # extract query string values
29
+ request['query'] = Addressable::URI.parse(request['path']).query_values ||= {}
30
+ # extract remaining headers
31
+ lines.each do |line|
32
+ h = HEADER.match(line)
33
+ request[h[1].strip.downcase] = h[2].strip if h
34
+ end
35
+
36
+ build_with_request(connection, request, remains, secure, debug)
37
+ end
38
+
39
+ def self.build_with_request(connection, request, remains, secure = false, debug = false)
40
+ # Determine version heuristically
41
+ version = if request['sec-websocket-version']
42
+ # Used from drafts 04 onwards
43
+ request['sec-websocket-version'].to_i
44
+ elsif request['sec-websocket-draft']
45
+ # Used in drafts 01 - 03
46
+ request['sec-websocket-draft'].to_i
47
+ elsif request['sec-websocket-key1']
48
+ 76
49
+ else
50
+ 75
51
+ end
52
+
53
+ # Additional handling of bytes after the header if required
54
+ case version
55
+ when 75
56
+ if !remains.empty?
57
+ raise HandshakeError, "Extra bytes after header"
58
+ end
59
+ when 76, 1..3
60
+ if remains.length < 8
61
+ # The whole third-key has not been received yet.
62
+ return nil
63
+ elsif remains.length > 8
64
+ raise HandshakeError, "Extra bytes after third key"
65
+ end
66
+ request['third-key'] = remains
67
+ end
68
+
69
+ # Validate that Connection and Upgrade headers
70
+ unless request['connection'] && request['connection'] =~ /Upgrade/ && request['upgrade'] && request['upgrade'].downcase == 'websocket'
71
+ raise HandshakeError, "Connection and Upgrade headers required"
72
+ end
73
+
74
+ # transform headers
75
+ protocol = (secure ? "wss" : "ws")
76
+ request['host'] = Addressable::URI.parse("#{protocol}://"+request['host'])
77
+
78
+ case version
79
+ when 75
80
+ Handler75.new(connection, request, debug)
81
+ when 76
82
+ Handler76.new(connection, request, debug)
83
+ when 1..3
84
+ # We'll use handler03 - I believe they're all compatible
85
+ Handler03.new(connection, request, debug)
86
+ when 5
87
+ Handler05.new(connection, request, debug)
88
+ when 6
89
+ Handler06.new(connection, request, debug)
90
+ when 7
91
+ Handler07.new(connection, request, debug)
92
+ when 8
93
+ # drafts 9, 10, 11 and 12 should never change the version
94
+ # number as they are all the same as version 08.
95
+ Handler08.new(connection, request, debug)
96
+ when 13
97
+ # drafts 13 to 17 all identify as version 13 as they are
98
+ # only minor changes or text changes.
99
+ Handler13.new(connection, request, debug)
100
+ else
101
+ # According to spec should abort the connection
102
+ raise WebSocketError, "Protocol version #{version} not supported"
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,75 @@
1
+ require 'digest/sha1'
2
+ require 'base64'
3
+
4
+ module EventMachine
5
+ module WebSocket
6
+ module Handshake04
7
+
8
+ def handshake_key_response(key)
9
+ string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
10
+ Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
11
+ end
12
+
13
+ def handshake_server
14
+ # Required
15
+ unless key = request['sec-websocket-key']
16
+ raise HandshakeError, "Sec-WebSocket-Key header is required"
17
+ end
18
+
19
+ # Optional
20
+ origin = request['sec-websocket-origin']
21
+ protocols = request['sec-websocket-protocol']
22
+ extensions = request['sec-websocket-extensions']
23
+
24
+ upgrade = ["HTTP/1.1 101 Switching Protocols"]
25
+ upgrade << "Upgrade: websocket"
26
+ upgrade << "Connection: Upgrade"
27
+ upgrade << "Sec-WebSocket-Accept: #{handshake_key_response(key)}"
28
+
29
+ # TODO: Support Sec-WebSocket-Protocol
30
+ # TODO: Sec-WebSocket-Extensions
31
+
32
+ [:upgrade_headers, upgrade]
33
+
34
+ return upgrade.join("\r\n") + "\r\n\r\n"
35
+ end
36
+
37
+ def handshake_client
38
+ request = ["GET /websocket HTTP/1.1"]
39
+ request << "Host: #{@request[:host]}:#{@request[:port]}"
40
+ request << "Connection: keep-alive, Upgrade"
41
+ request << "Sec-WebSocket-Version: 8" # TODO: supply version somehow
42
+ request << "Sec-WebSocket-Origin: null"
43
+ random16 = (0...16).map{rand(255).chr}.join
44
+ random16_base64 = Base64.encode64(random16).chomp
45
+ @correct_response = handshake_key_response random16_base64
46
+ request << "Sec-WebSocket-Key: #{random16_base64}"
47
+ request << "Upgrade: websocket"
48
+ # TODO: anything else needed? nothing else parsed anyway
49
+ return request.join("\r\n") + "\r\n\r\n"
50
+ end
51
+
52
+ def client_handle_server_handshake_response(data)
53
+ header, msg = data.split "\r\n\r\n"
54
+ lines = header.split("\r\n")
55
+ accept = false
56
+ lines.each do |line|
57
+ h = /^([^:]+):\s*(.+)$/.match(line)
58
+ if !h.nil? and h[1].strip.downcase == "sec-websocket-accept"
59
+ accept = (h[2] == @correct_response)
60
+ break
61
+ end
62
+ end
63
+ if accept
64
+ @state = :connected #TODO - some actual logic would be nice
65
+ @connection.trigger_on_open
66
+ if msg # handle message bundled in with handshake response
67
+ receive_data(msg)
68
+ end
69
+ else
70
+ close_websocket(1002,nil)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,21 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Handshake75
4
+ def handshake
5
+ location = "#{request['host'].scheme}://#{request['host'].host}"
6
+ location << ":#{request['host'].port}" if request['host'].port
7
+ location << request['path']
8
+
9
+ upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
+ upgrade << "Upgrade: WebSocket\r\n"
11
+ upgrade << "Connection: Upgrade\r\n"
12
+ upgrade << "WebSocket-Origin: #{request['origin']}\r\n"
13
+ upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
+
15
+ debug [:upgrade_headers, upgrade]
16
+
17
+ return upgrade
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,71 @@
1
+ require 'digest/md5'
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ module Handshake76
6
+ def handshake
7
+ challenge_response = solve_challenge(
8
+ request['sec-websocket-key1'],
9
+ request['sec-websocket-key2'],
10
+ request['third-key']
11
+ )
12
+
13
+ location = "#{request['host'].scheme}://#{request['host'].host}"
14
+ location << ":#{request['host'].port}" if request['host'].port
15
+ location << request['path']
16
+
17
+ upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
18
+ upgrade << "Upgrade: WebSocket\r\n"
19
+ upgrade << "Connection: Upgrade\r\n"
20
+ upgrade << "Sec-WebSocket-Location: #{location}\r\n"
21
+ upgrade << "Sec-WebSocket-Origin: #{request['origin']}\r\n"
22
+ if protocol = request['sec-websocket-protocol']
23
+ validate_protocol!(protocol)
24
+ upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
25
+ end
26
+ upgrade << "\r\n"
27
+ upgrade << challenge_response
28
+
29
+ debug [:upgrade_headers, upgrade]
30
+
31
+ return upgrade
32
+ end
33
+
34
+ private
35
+
36
+ def solve_challenge(first, second, third)
37
+ # Refer to 5.2 4-9 of the draft 76
38
+ sum = [numbers_over_spaces(first)].pack("N*") +
39
+ [numbers_over_spaces(second)].pack("N*") +
40
+ third
41
+ Digest::MD5.digest(sum)
42
+ end
43
+
44
+ def numbers_over_spaces(string)
45
+ numbers = string.scan(/[0-9]/).join.to_i
46
+
47
+ spaces = string.scan(/ /).size
48
+ # As per 5.2.5, abort the connection if spaces are zero.
49
+ raise HandshakeError, "Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack" if spaces == 0
50
+
51
+ # As per 5.2.6, abort if numbers is not an integral multiple of spaces
52
+ if numbers % spaces != 0
53
+ raise HandshakeError, "Invalid Key #{string.inspect}"
54
+ end
55
+
56
+ quotient = numbers / spaces
57
+
58
+ if quotient > 2**32-1
59
+ raise HandshakeError, "Challenge computation out of range for key #{string.inspect}"
60
+ end
61
+
62
+ return quotient
63
+ end
64
+
65
+ def validate_protocol!(protocol)
66
+ raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
67
+ # TODO: Validate characters
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,63 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class MaskedString < String
4
+ # Read a 4 bit XOR mask - further requested bytes will be unmasked
5
+ def read_mask
6
+ if respond_to?(:encoding) && encoding.name != "ASCII-8BIT"
7
+ raise "MaskedString only operates on BINARY strings"
8
+ end
9
+ raise "Too short" if bytesize < 4 # TODO - change
10
+ @masking_key = String.new(self[0..3])
11
+ end
12
+
13
+ def self.create_mask
14
+ MaskedString.new "rAnD" #TODO make random 4 character string
15
+ end
16
+
17
+ def self.create_masked_string(original)
18
+ masked_string = MaskedString.new
19
+ masking_key = self.create_mask
20
+ masked_string << masking_key
21
+ original.size.times do |i|
22
+ char = original.getbyte(i)
23
+ masked_string << (char ^ masking_key.getbyte(i%4))
24
+ end
25
+ if masked_string.respond_to?(:force_encoding)
26
+ masked_string.force_encoding("ASCII-8BIT")
27
+ end
28
+ masked_string.read_mask # get input string
29
+ return masked_string
30
+ end
31
+
32
+ # Removes the mask, behaves like a normal string again
33
+ def unset_mask
34
+ @masking_key = nil
35
+ end
36
+
37
+ def slice_mask
38
+ slice!(0, 4)
39
+ end
40
+
41
+ def getbyte(index)
42
+ if @masking_key
43
+ masked_char = super
44
+ masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def getbytes(start_index, count)
51
+ data = ''
52
+ if @masking_key
53
+ count.times do |i|
54
+ data << getbyte(start_index + i)
55
+ end
56
+ else
57
+ data = String.new(self[start_index..start_index+count])
58
+ end
59
+ data
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: BINARY
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ module MessageProcessor03
6
+ def message(message_type, extension_data, application_data)
7
+ case message_type
8
+ when :close
9
+ if @state == :closing
10
+ # TODO: Check that message body matches sent data
11
+ # We can close connection immediately since there is no more data
12
+ # is allowed to be sent or received on this connection
13
+ @connection.close_connection
14
+ @state = :closed
15
+ else
16
+ # Acknowlege close
17
+ # The connection is considered closed
18
+ send_frame(:close, application_data)
19
+ @state = :closed
20
+ @connection.close_connection_after_writing
21
+ end
22
+ when :ping
23
+ # Pong back the same data
24
+ send_frame(:pong, application_data)
25
+ when :pong
26
+ # TODO: Do something. Complete a deferrable established by a ping?
27
+ when :text
28
+ if application_data.respond_to?(:force_encoding)
29
+ application_data.force_encoding("UTF-8")
30
+ end
31
+ @connection.trigger_on_message(application_data)
32
+ when :binary
33
+ @connection.trigger_on_message(application_data)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module MessageProcessor06
4
+ def message(message_type, extension_data, application_data)
5
+ debug [:message_received, message_type, application_data]
6
+
7
+ case message_type
8
+ when :close
9
+ status_code = case application_data.length
10
+ when 0
11
+ # close messages MAY contain a body
12
+ nil
13
+ when 1
14
+ # Illegal close frame
15
+ raise DataError, "Close frames with a body must contain a 2 byte status code"
16
+ else
17
+ application_data.slice!(0, 2).unpack('n').first
18
+ end
19
+
20
+ debug [:close_frame_received, status_code, application_data]
21
+
22
+ if @state == :closing
23
+ # We can close connection immediately since there is no more data
24
+ # is allowed to be sent or received on this connection
25
+ @connection.close_connection
26
+ @state = :closed
27
+ else
28
+ # Acknowlege close
29
+ # The connection is considered closed
30
+ send_frame(:close, '')
31
+ @state = :closed
32
+ @connection.close_connection_after_writing
33
+ # TODO: Send close status code and body to app code
34
+ end
35
+ when :ping
36
+ # Pong back the same data
37
+ send_frame(:pong, application_data)
38
+ when :pong
39
+ # TODO: Do something. Complete a deferrable established by a ping?
40
+ @connection.trigger_on_message(application_data, :pong)
41
+ when :text
42
+ if application_data.respond_to?(:force_encoding)
43
+ application_data.force_encoding("UTF-8")
44
+ end
45
+ @connection.trigger_on_message(application_data, :text)
46
+ when :binary
47
+ @connection.trigger_on_message(application_data, :binary)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module Websocket
3
+ VERSION = "0.3.7"
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class WebSocketError < RuntimeError; end
4
+ class HandshakeError < WebSocketError; end
5
+ class DataError < WebSocketError; end
6
+
7
+ #backwards compatibility
8
+ def self.start(options, &blk)
9
+ self.start_ws_server(options, &blk)
10
+ end
11
+
12
+ def self.start_ws_server(options, &blk)
13
+ EM.epoll
14
+ EM.run do
15
+
16
+ #trap("TERM") { stop; }
17
+ #trap("INT") { stop; }
18
+
19
+ EventMachine::start_server(options[:host], options[:port],
20
+ EventMachine::WebSocket::Connection, options) do |c|
21
+ blk.call(c)
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.start_ws_client(options, &blk)
27
+ EM.epoll
28
+ EM.run do
29
+
30
+ #trap("TERM") { stop; raise "TERM" }
31
+ #trap("INT") { stop; raise "INT" }
32
+
33
+ EM.connect(options[:host], options[:port],
34
+ EventMachine::WebSocket::ClientConnection, options) do |c|
35
+ blk.call(c)
36
+ end
37
+ end
38
+ end
39
+
40
+ def self.stop
41
+ puts "Terminating WebSocket Server"
42
+ EventMachine.stop
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require "eventmachine"
4
+
5
+ %w[
6
+ debugger websocket connection client_connection
7
+ handshake75 handshake76 handshake04
8
+ framing76 framing03 framing04 framing05 framing07
9
+ close75 close03 close05 close06
10
+ masking04
11
+ message_processor_03 message_processor_06
12
+ handler_factory handler handler75 handler76 handler03 handler05 handler06 handler07 handler08 handler13
13
+ ].each do |file|
14
+ require "em-websocket/#{file}"
15
+ end
16
+
17
+ unless ''.respond_to?(:getbyte)
18
+ class String
19
+ def getbyte(i)
20
+ self[i]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1 @@
1
+ require "em-websocket"