em-websocket 0.3.7 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
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