em-websocket 0.3.7 → 0.5.2

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +52 -0
  3. data/Gemfile +6 -0
  4. data/LICENCE +7 -0
  5. data/README.md +105 -40
  6. data/examples/echo.rb +22 -6
  7. data/examples/test.html +5 -6
  8. data/lib/em-websocket.rb +2 -1
  9. data/lib/em-websocket/close03.rb +3 -0
  10. data/lib/em-websocket/close05.rb +3 -0
  11. data/lib/em-websocket/close06.rb +3 -0
  12. data/lib/em-websocket/close75.rb +2 -1
  13. data/lib/em-websocket/connection.rb +154 -48
  14. data/lib/em-websocket/framing03.rb +3 -2
  15. data/lib/em-websocket/framing05.rb +3 -2
  16. data/lib/em-websocket/framing07.rb +16 -4
  17. data/lib/em-websocket/framing76.rb +1 -4
  18. data/lib/em-websocket/handler.rb +61 -15
  19. data/lib/em-websocket/handler03.rb +0 -1
  20. data/lib/em-websocket/handler05.rb +0 -1
  21. data/lib/em-websocket/handler06.rb +0 -1
  22. data/lib/em-websocket/handler07.rb +0 -1
  23. data/lib/em-websocket/handler08.rb +0 -1
  24. data/lib/em-websocket/handler13.rb +0 -1
  25. data/lib/em-websocket/handler76.rb +2 -0
  26. data/lib/em-websocket/handshake.rb +156 -0
  27. data/lib/em-websocket/handshake04.rb +18 -16
  28. data/lib/em-websocket/handshake75.rb +15 -8
  29. data/lib/em-websocket/handshake76.rb +15 -14
  30. data/lib/em-websocket/masking04.rb +3 -6
  31. data/lib/em-websocket/message_processor_03.rb +6 -3
  32. data/lib/em-websocket/message_processor_06.rb +30 -9
  33. data/lib/em-websocket/version.rb +1 -1
  34. data/lib/em-websocket/websocket.rb +24 -15
  35. data/spec/helper.rb +84 -51
  36. data/spec/integration/common_spec.rb +89 -69
  37. data/spec/integration/draft03_spec.rb +84 -56
  38. data/spec/integration/draft05_spec.rb +14 -12
  39. data/spec/integration/draft06_spec.rb +67 -7
  40. data/spec/integration/draft13_spec.rb +30 -19
  41. data/spec/integration/draft75_spec.rb +46 -40
  42. data/spec/integration/draft76_spec.rb +59 -45
  43. data/spec/integration/gte_03_examples.rb +42 -0
  44. data/spec/integration/shared_examples.rb +119 -0
  45. data/spec/unit/framing_spec.rb +24 -4
  46. data/spec/unit/handshake_spec.rb +216 -0
  47. data/spec/unit/masking_spec.rb +2 -0
  48. metadata +32 -86
  49. data/examples/flash_policy_file_server.rb +0 -21
  50. data/examples/js/FABridge.js +0 -604
  51. data/examples/js/WebSocketMain.swf +0 -0
  52. data/examples/js/swfobject.js +0 -4
  53. data/examples/js/web_socket.js +0 -312
  54. data/lib/em-websocket/handler_factory.rb +0 -109
  55. data/spec/unit/handler_spec.rb +0 -159
@@ -6,9 +6,10 @@ module EventMachine
6
6
  def initialize_framing
7
7
  @data = ''
8
8
  @application_data_buffer = '' # Used for MORE frames
9
+ @frame_type = nil
9
10
  end
10
11
 
11
- def process_data(newdata)
12
+ def process_data
12
13
  error = false
13
14
 
14
15
  while !error && @data.size > 1
@@ -150,7 +151,7 @@ module EventMachine
150
151
  end
151
152
 
152
153
  def opcode_to_type(opcode)
153
- FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode")
154
+ FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
154
155
  end
155
156
 
156
157
  def data_frame?(type)
@@ -6,9 +6,10 @@ module EventMachine
6
6
  def initialize_framing
7
7
  @data = MaskedString.new
8
8
  @application_data_buffer = '' # Used for MORE frames
9
+ @frame_type = nil
9
10
  end
10
11
 
11
- def process_data(newdata)
12
+ def process_data
12
13
  error = false
13
14
 
14
15
  while !error && @data.size > 5 # mask plus first byte present
@@ -151,7 +152,7 @@ module EventMachine
151
152
  end
152
153
 
153
154
  def opcode_to_type(opcode)
154
- FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode")
155
+ FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
155
156
  end
156
157
 
157
158
  def data_frame?(type)
@@ -7,9 +7,10 @@ module EventMachine
7
7
  def initialize_framing
8
8
  @data = MaskedString.new
9
9
  @application_data_buffer = '' # Used for MORE frames
10
+ @frame_type = nil
10
11
  end
11
12
 
12
- def process_data(newdata)
13
+ def process_data
13
14
  error = false
14
15
 
15
16
  while !error && @data.size >= 2
@@ -86,8 +87,19 @@ module EventMachine
86
87
 
87
88
  frame_type = opcode_to_type(opcode)
88
89
 
89
- if frame_type == :continuation && !@frame_type
90
- raise WSProtocolError, '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'
91
103
  end
92
104
 
93
105
  if !fin
@@ -162,7 +174,7 @@ module EventMachine
162
174
  end
163
175
 
164
176
  def opcode_to_type(opcode)
165
- FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode")
177
+ FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
166
178
  end
167
179
 
168
180
  def data_frame?(type)
@@ -7,7 +7,7 @@ module EventMachine
7
7
  @data = ''
8
8
  end
9
9
 
10
- def process_data(newdata)
10
+ def process_data
11
11
  debug [:message, @data]
12
12
 
13
13
  # This algorithm comes straight from the spec
@@ -71,9 +71,6 @@ module EventMachine
71
71
  raise WSMessageTooBigError, "Frame length too long (#{@data.size} bytes)"
72
72
  end
73
73
 
74
- # Optimization to avoid calling slice! unnecessarily
75
- error = true and next unless newdata =~ /\xff/
76
-
77
74
  msg = @data.slice!(/\A\x00[^\xff]*\xff/)
78
75
  if msg
79
76
  msg.gsub!(/\A\x00|\xff\z/, '')
@@ -1,40 +1,86 @@
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
40
+ @state = :connected
41
+ @close_timer = nil
12
42
  initialize_framing
13
43
  end
14
44
 
15
- def run
16
- @connection.send_data handshake
17
- @state = :connected
18
- @connection.trigger_on_open
45
+ def receive_data(data)
46
+ @data << data
47
+ process_data
48
+ rescue WSProtocolError => e
49
+ fail_websocket(e)
19
50
  end
20
51
 
21
- # Handshake response
22
- def handshake
52
+ def close_websocket(code, body)
23
53
  # Implemented in subclass
24
54
  end
25
55
 
26
- def receive_data(data)
27
- @data << data
28
- process_data(data)
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
+ }
29
63
  end
30
64
 
31
- def close_websocket(code, body)
32
- # 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)
33
71
  end
34
72
 
35
73
  def unbind
36
74
  @state = :closed
37
- @connection.trigger_on_close
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)
38
84
  end
39
85
 
40
86
  def ping
@@ -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
@@ -4,32 +4,34 @@ require 'base64'
4
4
  module EventMachine
5
5
  module WebSocket
6
6
  module Handshake04
7
- def handshake
7
+ def self.handshake(headers, _, __)
8
8
  # Required
9
- unless key = request['sec-websocket-key']
10
- raise HandshakeError, "Sec-WebSocket-Key header is required"
9
+ unless key = headers['sec-websocket-key']
10
+ raise HandshakeError, "sec-websocket-key header is required"
11
11
  end
12
-
13
- # Optional
14
- origin = request['sec-websocket-origin']
15
- protocols = request['sec-websocket-protocol']
16
- extensions = request['sec-websocket-extensions']
17
-
12
+
18
13
  string_to_sign = "#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
19
14
  signature = Base64.encode64(Digest::SHA1.digest(string_to_sign)).chomp
20
-
15
+
21
16
  upgrade = ["HTTP/1.1 101 Switching Protocols"]
22
17
  upgrade << "Upgrade: websocket"
23
18
  upgrade << "Connection: Upgrade"
24
19
  upgrade << "Sec-WebSocket-Accept: #{signature}"
25
-
26
- # TODO: Support Sec-WebSocket-Protocol
27
- # TODO: Sec-WebSocket-Extensions
28
-
29
- debug [:upgrade_headers, upgrade]
30
-
20
+ if protocol = headers['sec-websocket-protocol']
21
+ validate_protocol!(protocol)
22
+ upgrade << "Sec-WebSocket-Protocol: #{protocol}"
23
+ end
24
+
25
+ # TODO: Support sec-websocket-protocol selection
26
+ # TODO: sec-websocket-extensions
27
+
31
28
  return upgrade.join("\r\n") + "\r\n\r\n"
32
29
  end
30
+
31
+ def self.validate_protocol!(protocol)
32
+ raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
33
+ # TODO: Validate characters
34
+ end
33
35
  end
34
36
  end
35
37
  end