sonixlabs-em-websocket 0.3.8 → 0.5.1.1

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +69 -0
  3. data/Gemfile +6 -0
  4. data/LICENCE +7 -0
  5. data/README.md +100 -56
  6. data/README.md.BACKUP.14928.md +195 -0
  7. data/README.md.BASE.14928.md +77 -0
  8. data/README.md.LOCAL.14928.md +98 -0
  9. data/README.md.REMOTE.14928.md +142 -0
  10. data/examples/echo.rb +23 -7
  11. data/examples/ping.rb +24 -0
  12. data/examples/test.html +5 -6
  13. data/lib/em-websocket.rb +4 -2
  14. data/lib/em-websocket/close03.rb +3 -0
  15. data/lib/em-websocket/close05.rb +3 -0
  16. data/lib/em-websocket/close06.rb +3 -0
  17. data/lib/em-websocket/close75.rb +2 -1
  18. data/lib/em-websocket/connection.rb +219 -73
  19. data/lib/em-websocket/framing03.rb +6 -11
  20. data/lib/em-websocket/framing05.rb +6 -11
  21. data/lib/em-websocket/framing07.rb +25 -20
  22. data/lib/em-websocket/framing76.rb +6 -15
  23. data/lib/em-websocket/handler.rb +69 -28
  24. data/lib/em-websocket/handler03.rb +0 -1
  25. data/lib/em-websocket/handler05.rb +0 -1
  26. data/lib/em-websocket/handler06.rb +0 -1
  27. data/lib/em-websocket/handler07.rb +0 -1
  28. data/lib/em-websocket/handler08.rb +0 -1
  29. data/lib/em-websocket/handler13.rb +0 -1
  30. data/lib/em-websocket/handler76.rb +2 -0
  31. data/lib/em-websocket/handshake.rb +156 -0
  32. data/lib/em-websocket/handshake04.rb +18 -56
  33. data/lib/em-websocket/handshake75.rb +15 -8
  34. data/lib/em-websocket/handshake76.rb +15 -14
  35. data/lib/em-websocket/masking04.rb +4 -30
  36. data/lib/em-websocket/message_processor_03.rb +13 -4
  37. data/lib/em-websocket/message_processor_06.rb +25 -13
  38. data/lib/em-websocket/version.rb +1 -1
  39. data/lib/em-websocket/websocket.rb +35 -24
  40. data/spec/helper.rb +82 -55
  41. data/spec/integration/common_spec.rb +90 -70
  42. data/spec/integration/draft03_spec.rb +84 -56
  43. data/spec/integration/draft05_spec.rb +14 -12
  44. data/spec/integration/draft06_spec.rb +66 -9
  45. data/spec/integration/draft13_spec.rb +59 -29
  46. data/spec/integration/draft75_spec.rb +46 -40
  47. data/spec/integration/draft76_spec.rb +113 -109
  48. data/spec/integration/gte_03_examples.rb +42 -0
  49. data/spec/integration/shared_examples.rb +174 -0
  50. data/spec/unit/framing_spec.rb +83 -110
  51. data/spec/unit/handshake_spec.rb +216 -0
  52. data/spec/unit/masking_spec.rb +2 -0
  53. metadata +31 -71
  54. data/examples/flash_policy_file_server.rb +0 -21
  55. data/examples/js/FABridge.js +0 -604
  56. data/examples/js/WebSocketMain.swf +0 -0
  57. data/examples/js/swfobject.js +0 -4
  58. data/examples/js/web_socket.js +0 -312
  59. data/lib/em-websocket/handler_factory.rb +0 -107
  60. data/spec/unit/handler_spec.rb +0 -147
@@ -3,17 +3,13 @@
3
3
  module EventMachine
4
4
  module WebSocket
5
5
  module Framing05
6
-
7
- # Set the max frame lenth to very high value (10MB) until there is a
8
- # limit specified in the spec to protect against malicious attacks
9
- MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
10
-
11
6
  def initialize_framing
12
7
  @data = MaskedString.new
13
8
  @application_data_buffer = '' # Used for MORE frames
9
+ @frame_type = nil
14
10
  end
15
11
 
16
- def process_data(newdata)
12
+ def process_data
17
13
  error = false
18
14
 
19
15
  while !error && @data.size > 5 # mask plus first byte present
@@ -60,9 +56,8 @@ module EventMachine
60
56
  length
61
57
  end
62
58
 
63
- # Addition to the spec to protect against malicious requests
64
- if payload_length > MAXIMUM_FRAME_LENGTH
65
- raise DataError, "Frame length too long (#{payload_length} bytes)"
59
+ if payload_length > @connection.max_frame_size
60
+ raise WSMessageTooBigError, "Frame length too long (#{payload_length} bytes)"
66
61
  end
67
62
 
68
63
  # Check buffer size
@@ -83,7 +78,7 @@ module EventMachine
83
78
  frame_type = opcode_to_type(opcode)
84
79
 
85
80
  if frame_type == :continuation && !@frame_type
86
- raise WebSocketError, 'Continuation frame not expected'
81
+ raise WSProtocolError, 'Continuation frame not expected'
87
82
  end
88
83
 
89
84
  if !fin
@@ -157,7 +152,7 @@ module EventMachine
157
152
  end
158
153
 
159
154
  def opcode_to_type(opcode)
160
- FRAME_TYPES_INVERSE[opcode] || raise(DataError, "Unknown opcode")
155
+ FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
161
156
  end
162
157
 
163
158
  def data_frame?(type)
@@ -4,16 +4,13 @@ module EventMachine
4
4
  module WebSocket
5
5
  module Framing07
6
6
 
7
- attr_accessor :mask_outbound_messages, :require_masked_inbound_messages
8
-
9
7
  def initialize_framing
10
8
  @data = MaskedString.new
11
9
  @application_data_buffer = '' # Used for MORE frames
12
- @mask_outbound_messages = false
13
- @require_masked_inbound_messages = true
10
+ @frame_type = nil
14
11
  end
15
12
 
16
- def process_data(newdata)
13
+ def process_data
17
14
  error = false
18
15
 
19
16
  while !error && @data.size >= 2
@@ -28,9 +25,7 @@ module EventMachine
28
25
  length = @data.getbyte(pointer) & 0b01111111
29
26
  pointer += 1
30
27
 
31
- if require_masked_inbound_messages
32
- raise WebSocketError, 'Data from client must be masked' unless mask
33
- end
28
+ # raise WebSocketError, 'Data from client must be masked' unless mask
34
29
 
35
30
  payload_length = case length
36
31
  when 127 # Length defined by 8 bytes
@@ -65,6 +60,10 @@ module EventMachine
65
60
  frame_length = pointer + payload_length
66
61
  frame_length += 4 if mask
67
62
 
63
+ if frame_length > @connection.max_frame_size
64
+ raise WSMessageTooBigError, "Frame length too long (#{frame_length} bytes)"
65
+ end
66
+
68
67
  # Check buffer size
69
68
  if @data.getbyte(frame_length - 1) == nil
70
69
  debug [:buffer_incomplete, @data]
@@ -88,8 +87,19 @@ module EventMachine
88
87
 
89
88
  frame_type = opcode_to_type(opcode)
90
89
 
91
- if frame_type == :continuation && !@frame_type
92
- raise WebSocketError, 'Continuation frame not expected'
90
+ if frame_type == :continuation
91
+ if !@frame_type
92
+ raise WSProtocolError, 'Continuation frame not expected'
93
+ end
94
+ else # Not a continuation frame
95
+ if @frame_type && data_frame?(frame_type)
96
+ raise WSProtocolError, "Continuation frame expected"
97
+ end
98
+ end
99
+
100
+ # Validate that control frames are not fragmented
101
+ if !fin && !data_frame?(frame_type)
102
+ raise WSProtocolError, 'Control frames must not be fragmented'
93
103
  end
94
104
 
95
105
  if !fin
@@ -124,24 +134,19 @@ module EventMachine
124
134
  byte1 = opcode | 0b10000000 # fin bit set, rsv1-3 are 0
125
135
  frame << byte1
126
136
 
127
- mask = mask_outbound_messages ? 0b10000000 : 0b00000000 # must be masked if from client
128
137
  length = application_data.size
129
138
  if length <= 125
130
139
  byte2 = length # since rsv4 is 0
131
- frame << (mask | byte2)
140
+ frame << byte2
132
141
  elsif length < 65536 # write 2 byte length
133
- frame << (mask | 126)
142
+ frame << 126
134
143
  frame << [length].pack('n')
135
144
  else # write 8 byte length
136
- frame << (mask | 127)
145
+ frame << 127
137
146
  frame << [length >> 32, length & 0xFFFFFFFF].pack("NN")
138
147
  end
139
148
 
140
- if mask_outbound_messages
141
- frame << MaskedString.create_masked_string(application_data)
142
- else
143
- frame << application_data
144
- end
149
+ frame << application_data
145
150
 
146
151
  @connection.send_data(frame)
147
152
  end
@@ -169,7 +174,7 @@ module EventMachine
169
174
  end
170
175
 
171
176
  def opcode_to_type(opcode)
172
- FRAME_TYPES_INVERSE[opcode] || raise(DataError, "Unknown opcode")
177
+ FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
173
178
  end
174
179
 
175
180
  def data_frame?(type)
@@ -3,16 +3,11 @@
3
3
  module EventMachine
4
4
  module WebSocket
5
5
  module Framing76
6
-
7
- # Set the max frame lenth to very high value (10MB) until there is a
8
- # limit specified in the spec to protect against malicious attacks
9
- MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
10
-
11
6
  def initialize_framing
12
7
  @data = ''
13
8
  end
14
9
 
15
- def process_data(newdata)
10
+ def process_data
16
11
  debug [:message, @data]
17
12
 
18
13
  # This algorithm comes straight from the spec
@@ -40,9 +35,8 @@ module EventMachine
40
35
  break unless (b & 0x80) == 0x80
41
36
  end
42
37
 
43
- # Addition to the spec to protect against malicious requests
44
- if length > MAXIMUM_FRAME_LENGTH
45
- raise DataError, "Frame length too long (#{length} bytes)"
38
+ if length > @connection.max_frame_size
39
+ raise WSMessageTooBigError, "Frame length too long (#{length} bytes)"
46
40
  end
47
41
 
48
42
  if @data.getbyte(pointer+length-1) == nil
@@ -69,17 +63,14 @@ module EventMachine
69
63
 
70
64
  if @data.getbyte(0) != 0x00
71
65
  # Close the connection since this buffer can never match
72
- raise DataError, "Invalid frame received"
66
+ raise WSProtocolError, "Invalid frame received"
73
67
  end
74
68
 
75
69
  # Addition to the spec to protect against malicious requests
76
- if @data.size > MAXIMUM_FRAME_LENGTH
77
- raise DataError, "Frame length too long (#{@data.size} bytes)"
70
+ if @data.size > @connection.max_frame_size
71
+ raise WSMessageTooBigError, "Frame length too long (#{@data.size} bytes)"
78
72
  end
79
73
 
80
- # Optimization to avoid calling slice! unnecessarily
81
- error = true and next unless newdata =~ /\xff/
82
-
83
74
  msg = @data.slice!(/\A\x00[^\xff]*\xff/)
84
75
  if msg
85
76
  msg.gsub!(/\A\x00|\xff\z/, '')
@@ -1,55 +1,96 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler
4
+ def self.klass_factory(version)
5
+ case version
6
+ when 75
7
+ Handler75
8
+ when 76
9
+ Handler76
10
+ when 1..3
11
+ # We'll use handler03 - I believe they're all compatible
12
+ Handler03
13
+ when 5
14
+ Handler05
15
+ when 6
16
+ Handler06
17
+ when 7
18
+ Handler07
19
+ when 8
20
+ # drafts 9, 10, 11 and 12 should never change the version
21
+ # number as they are all the same as version 08.
22
+ Handler08
23
+ when 13
24
+ # drafts 13 to 17 all identify as version 13 as they are
25
+ # only minor changes or text changes.
26
+ Handler13
27
+ else
28
+ # According to spec should abort the connection
29
+ raise HandshakeError, "Protocol version #{version} not supported"
30
+ end
31
+ end
32
+
4
33
  include Debugger
5
34
 
6
35
  attr_reader :request, :state
7
36
 
8
- def initialize(connection, request, debug = false)
9
- @connection, @request = connection, request
37
+ def initialize(connection, debug = false)
38
+ @connection = connection
10
39
  @debug = debug
11
- @state = :handshake
12
- initialize_framing
13
- end
14
-
15
- def run_server
16
- @connection.send_data handshake_server
17
40
  @state = :connected
18
- @connection.trigger_on_open
41
+ @close_timer = nil
42
+ initialize_framing
19
43
  end
20
44
 
21
- def run_client
22
- self.mask_outbound_messages = true
23
- self.require_masked_inbound_messages = false
24
- @connection.send_data handshake_client
45
+ def receive_data(data)
46
+ @data << data
47
+ process_data
48
+ rescue WSProtocolError => e
49
+ fail_websocket(e)
25
50
  end
26
51
 
27
- # Handshake response
28
- def handshake
52
+ def close_websocket(code, body)
29
53
  # Implemented in subclass
30
54
  end
31
55
 
32
- def handshake_server
33
- handshake #backwards compatibility
56
+ # Used to avoid un-acked and unclosed remaining open indefinitely
57
+ def start_close_timeout
58
+ @close_timer = EM::Timer.new(@connection.close_timeout) {
59
+ @connection.close_connection
60
+ e = WSProtocolError.new("Close handshake un-acked after #{@connection.close_timeout}s, closing tcp connection")
61
+ @connection.trigger_on_error(e)
62
+ }
34
63
  end
35
64
 
36
- # Handshake initiation
37
- def handshake_client
38
- # Implemented in subclass
65
+ # This corresponds to "Fail the WebSocket Connection" in the spec.
66
+ def fail_websocket(e)
67
+ debug [:error, e]
68
+ close_websocket(e.code, e.message)
69
+ @connection.close_connection_after_writing
70
+ @connection.trigger_on_error(e)
39
71
  end
40
72
 
41
- def receive_data(data)
42
- @data << data
43
- process_data(data)
73
+ def unbind
74
+ @state = :closed
75
+
76
+ @close_timer.cancel if @close_timer
77
+
78
+ @close_info = defined?(@close_info) ? @close_info : {
79
+ :code => 1006,
80
+ :was_clean => false,
81
+ }
82
+
83
+ @connection.trigger_on_close(@close_info)
44
84
  end
45
85
 
46
- def close_websocket(code, body)
47
- # Implemented in subclass
86
+ def ping
87
+ # Overridden in subclass
88
+ false
48
89
  end
49
90
 
50
- def unbind
51
- @state = :closed
52
- @connection.trigger_on_close
91
+ def pingable?
92
+ # Also Overridden
93
+ false
53
94
  end
54
95
  end
55
96
  end
@@ -1,7 +1,6 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler03 < Handler
4
- include Handshake76
5
4
  include Framing03
6
5
  include MessageProcessor03
7
6
  include Close03
@@ -1,7 +1,6 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler05 < Handler
4
- include Handshake04
5
4
  include Framing05
6
5
  include MessageProcessor03
7
6
  include Close05
@@ -1,7 +1,6 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler06 < Handler
4
- include Handshake04
5
4
  include Framing05
6
5
  include MessageProcessor06
7
6
  include Close06
@@ -1,7 +1,6 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler07 < Handler
4
- include Handshake04
5
4
  include Framing07
6
5
  include MessageProcessor06
7
6
  include Close06
@@ -1,7 +1,6 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler08 < Handler
4
- include Handshake04
5
4
  include Framing07
6
5
  include MessageProcessor06
7
6
  include Close06
@@ -1,7 +1,6 @@
1
1
  module EventMachine
2
2
  module WebSocket
3
3
  class Handler13 < Handler
4
- include Handshake04
5
4
  include Framing07
6
5
  include MessageProcessor06
7
6
  include Close06
@@ -1,3 +1,5 @@
1
+ # encoding: BINARY
2
+
1
3
  module EventMachine
2
4
  module WebSocket
3
5
  class Handler76 < Handler
@@ -0,0 +1,156 @@
1
+ require "http/parser"
2
+ require "uri"
3
+
4
+ module EventMachine
5
+ module WebSocket
6
+
7
+ # Resposible for creating the server handshake response
8
+ class Handshake
9
+ include EM::Deferrable
10
+
11
+ attr_reader :parser, :protocol_version
12
+
13
+ # Unfortunately drafts 75 & 76 require knowledge of whether the
14
+ # connection is being terminated as ws/wss in order to generate the
15
+ # correct handshake response
16
+ def initialize(secure)
17
+ @parser = Http::Parser.new
18
+ @secure = secure
19
+
20
+ @parser.on_headers_complete = proc { |headers|
21
+ @headers = Hash[headers.map { |k,v| [k.downcase, v] }]
22
+ }
23
+ end
24
+
25
+ def receive_data(data)
26
+ @parser << data
27
+
28
+ if defined? @headers
29
+ process(@headers, @parser.upgrade_data)
30
+ end
31
+ rescue HTTP::Parser::Error => e
32
+ fail(HandshakeError.new("Invalid HTTP header: #{e.message}"))
33
+ end
34
+
35
+ # Returns the WebSocket upgrade headers as a hash.
36
+ #
37
+ # Keys are strings, unmodified from the request.
38
+ #
39
+ def headers
40
+ @parser.headers
41
+ end
42
+
43
+ # The same as headers, except that the hash keys are downcased
44
+ #
45
+ def headers_downcased
46
+ @headers
47
+ end
48
+
49
+ # Returns the request path (excluding any query params)
50
+ #
51
+ def path
52
+ @path
53
+ end
54
+
55
+ # Returns the query params as a string foo=bar&baz=...
56
+ def query_string
57
+ @query_string
58
+ end
59
+
60
+ def query
61
+ Hash[query_string.split('&').map { |c| c.split('=', 2) }]
62
+ end
63
+
64
+ # Returns the WebSocket origin header if provided
65
+ #
66
+ def origin
67
+ @headers["origin"] || @headers["sec-websocket-origin"] || nil
68
+ end
69
+
70
+ def secure?
71
+ @secure
72
+ end
73
+
74
+ private
75
+
76
+ def process(headers, remains)
77
+ unless @parser.http_method == "GET"
78
+ raise HandshakeError, "Must be GET request"
79
+ end
80
+
81
+ # Validate request path
82
+ #
83
+ # According to http://tools.ietf.org/search/rfc2616#section-5.1.2, an
84
+ # invalid Request-URI should result in a 400 status code, but
85
+ # HandshakeError's currently result in a WebSocket abort. It's not
86
+ # clear which should take precedence, but an abort will do just fine.
87
+ begin
88
+ uri = URI.parse(@parser.request_url)
89
+ @path = uri.path
90
+ @query_string = uri.query || ""
91
+ rescue URI::InvalidURIError
92
+ raise HandshakeError, "Invalid request URI: #{@parser.request_url}"
93
+ end
94
+
95
+ # Validate Upgrade
96
+ unless @parser.upgrade?
97
+ raise HandshakeError, "Not an upgrade request"
98
+ end
99
+ upgrade = @headers['upgrade']
100
+ unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
101
+ raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}"
102
+ end
103
+
104
+ # Determine version heuristically
105
+ version = if @headers['sec-websocket-version']
106
+ # Used from drafts 04 onwards
107
+ @headers['sec-websocket-version'].to_i
108
+ elsif @headers['sec-websocket-draft']
109
+ # Used in drafts 01 - 03
110
+ @headers['sec-websocket-draft'].to_i
111
+ elsif @headers['sec-websocket-key1']
112
+ 76
113
+ else
114
+ 75
115
+ end
116
+
117
+ # Additional handling of bytes after the header if required
118
+ case version
119
+ when 75
120
+ if !remains.empty?
121
+ raise HandshakeError, "Extra bytes after header"
122
+ end
123
+ when 76, 1..3
124
+ if remains.length < 8
125
+ # The whole third-key has not been received yet.
126
+ return nil
127
+ elsif remains.length > 8
128
+ raise HandshakeError, "Extra bytes after third key"
129
+ end
130
+ @headers['third-key'] = remains
131
+ end
132
+
133
+ handshake_klass = case version
134
+ when 75
135
+ Handshake75
136
+ when 76, 1..3
137
+ Handshake76
138
+ when 5, 6, 7, 8, 13
139
+ Handshake04
140
+ else
141
+ # According to spec should abort the connection
142
+ raise HandshakeError, "Protocol version #{version} not supported"
143
+ end
144
+
145
+ upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)
146
+
147
+ handler_klass = Handler.klass_factory(version)
148
+
149
+ @protocol_version = version
150
+ succeed(upgrade_response, handler_klass)
151
+ rescue HandshakeError => e
152
+ fail(e)
153
+ end
154
+ end
155
+ end
156
+ end