em-websocket 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,21 @@
1
1
  = Changelog
2
2
 
3
- == 0.4.0 / ?
3
+ == 0.5.0 / 2013-03-05
4
+
5
+ - new features:
6
+ - 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.
7
+ - use Connection#support_close_codes? to easily check whether close codes are supported by the WebSocket protocol (drafts 06 and above)
8
+ - closes connection with 1007 close code if text frame contains invalid UTF8
9
+ - added Handshake#secure? for checking whether the connection is secure (either ssl or behind an ssl proxy)
10
+
11
+ - changed:
12
+ - Defaults to sending no close code rather than 1000 (consistent with browsers)
13
+ - Allows sending a 3xxx close code
14
+ - Renamed Connection#close_websocket to Connection#close (again for consistency with browsers). Old method is available till 0.6.
15
+ - Sends reasons with internally generated closure (previously only sent code)
16
+ - Echos close code when replying to close handshake
17
+
18
+ == 0.4.0 / 2013-01-22
4
19
 
5
20
  - new features:
6
21
  - on_open handler is now passed a handshake object which exposes the request headers, path, and query parameters
data/README.md CHANGED
@@ -31,6 +31,45 @@ EM.run {
31
31
  }
32
32
  ```
33
33
 
34
+ ## Protocols supported, and protocol specific functionality
35
+
36
+ Supports all WebSocket protocols in use in the wild (and a few that are not): drafts 75, 76, 1-17, rfc.
37
+
38
+ 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
39
+
40
+ ### Ping & pong supported
41
+
42
+ Call `ws.pingable?` to check whether ping & pong is supported by the protocol in use.
43
+
44
+ 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.
45
+
46
+ 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.
47
+
48
+ ### Close codes and reasons
49
+
50
+ A WebSocket connection can be closed cleanly, regardless of protocol, by calling `ws.close(code = nil, body = nil)`.
51
+
52
+ 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).
53
+
54
+ The `onclose` callback is passed a hash which may contain following keys (depending on the protocol version):
55
+
56
+ * `was_clean`: boolean indicating whether the connection was closed via the close handshake.
57
+ * `code`: the close code. There are two special close codes which the server may set (as defined in the WebSocket spec):
58
+ * 1005: no code was supplied
59
+ * 1006: abnormal closure (the same as `was_clean: false`)
60
+ * `reason`: the close reason
61
+
62
+ Acceptable close codes are defined in the WebSocket rfc (<http://tools.ietf.org/html/rfc6455#section-7.4>). The following codes can be supplies when calling `ws.close(code)`:
63
+
64
+ * 1000: a generic normal close
65
+ * range 3xxx: reserved for libraries, frameworks, and applications (and can be registered with IANA)
66
+ * range 4xxx: for private use
67
+
68
+ If unsure use a code in the 4xxx range. em-websocket may also close a connection with one of the following close codes:
69
+
70
+ * 1002: WebSocket protocol error.
71
+ * 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`).
72
+
34
73
  ## Secure server
35
74
 
36
75
  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
@@ -51,6 +90,8 @@ EM::WebSocket.start({
51
90
  end
52
91
  ```
53
92
 
93
+ It's possible to check whether an incoming connection is secure by reading `handshake.secure?` in the onopen callback.
94
+
54
95
  ## Running behind an SSL Proxy/Terminator, like Stunnel
55
96
 
56
97
  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.
@@ -101,4 +142,4 @@ Using flash emulation does require some minimal support from em-websocket which
101
142
 
102
143
  # License
103
144
 
104
- The MIT License - Copyright (c) 2009 Ilya Grigorik
145
+ The MIT License - Copyright (c) 2009-2013 Ilya Grigorik, Martyn Loughran
@@ -12,7 +12,9 @@
12
12
  var Socket = "MozWebSocket" in window ? MozWebSocket : WebSocket;
13
13
  var ws = new Socket("ws://localhost:8080/foo/bar?hello=world");
14
14
  ws.onmessage = function(evt) { debug("Received: " + evt.data); };
15
- ws.onclose = function() { debug("socket closed"); };
15
+ ws.onclose = function(event) {
16
+ debug("Closed - code: " + event.code + ", reason: " + event.reason + ", wasClean: " + event.wasClean);
17
+ };
16
18
  ws.onopen = function() {
17
19
  debug("connected...");
18
20
  ws.send("hello server");
@@ -6,6 +6,8 @@ module EventMachine
6
6
  send_frame(:close, '')
7
7
  @state = :closing
8
8
  end
9
+
10
+ def supports_close_codes?; false; end
9
11
  end
10
12
  end
11
13
  end
@@ -6,6 +6,8 @@ module EventMachine
6
6
  send_frame(:close, "\x53")
7
7
  @state = :closing
8
8
  end
9
+
10
+ def supports_close_codes?; false; end
9
11
  end
10
12
  end
11
13
  end
@@ -11,6 +11,8 @@ module EventMachine
11
11
  end
12
12
  @state = :closing
13
13
  end
14
+
15
+ def supports_close_codes?; true; end
14
16
  end
15
17
  end
16
18
  end
@@ -2,9 +2,10 @@ module EventMachine
2
2
  module WebSocket
3
3
  module Close75
4
4
  def close_websocket(code, body)
5
- @state = :closed
6
5
  @connection.close_connection_after_writing
7
6
  end
7
+
8
+ def supports_close_codes?; false; end
8
9
  end
9
10
  end
10
11
  end
@@ -14,22 +14,22 @@ module EventMachine
14
14
  def onpong(&blk); @onpong = blk; end
15
15
 
16
16
  def trigger_on_message(msg)
17
- @onmessage.call(msg) if @onmessage
17
+ @onmessage.call(msg) if defined? @onmessage
18
18
  end
19
19
  def trigger_on_open(handshake)
20
- @onopen.call(handshake) if @onopen
20
+ @onopen.call(handshake) if defined? @onopen
21
21
  end
22
- def trigger_on_close
23
- @onclose.call if @onclose
22
+ def trigger_on_close(event = {})
23
+ @onclose.call(event) if defined? @onclose
24
24
  end
25
25
  def trigger_on_ping(data)
26
- @onping.call(data) if @onping
26
+ @onping.call(data) if defined? @onping
27
27
  end
28
28
  def trigger_on_pong(data)
29
- @onpong.call(data) if @onpong
29
+ @onpong.call(data) if defined? @onpong
30
30
  end
31
31
  def trigger_on_error(reason)
32
- return false unless @onerror
32
+ return false unless defined? @onerror
33
33
  @onerror.call(reason)
34
34
  true
35
35
  end
@@ -41,23 +41,25 @@ module EventMachine
41
41
  @secure_proxy = options[:secure_proxy] || false
42
42
  @tls_options = options[:tls_options] || {}
43
43
 
44
+ @handler = nil
45
+
44
46
  debug [:initialize]
45
47
  end
46
48
 
47
49
  # Use this method to close the websocket connection cleanly
48
50
  # This sends a close frame and waits for acknowlegement before closing
49
51
  # the connection
50
- def close_websocket(code = nil, body = nil)
51
- if code && !(4000..4999).include?(code)
52
- raise "Application code may only use codes in the range 4000-4999"
52
+ def close(code = nil, body = nil)
53
+ if code && !acceptable_close_code?(code)
54
+ raise "Application code may only use codes from 1000, 3000-4999"
53
55
  end
54
56
 
55
- # If code not defined then set to 1000 (normal closure)
56
- code ||= 1000
57
-
58
57
  close_websocket_private(code, body)
59
58
  end
60
59
 
60
+ # Deprecated, to be removed in version 0.6
61
+ alias :close_websocket :close
62
+
61
63
  def post_init
62
64
  start_tls(@tls_options) if @secure
63
65
  end
@@ -73,14 +75,16 @@ module EventMachine
73
75
  rescue WSProtocolError => e
74
76
  debug [:error, e]
75
77
  trigger_on_error(e)
76
- close_websocket_private(e.code)
78
+ close_websocket_private(e.code, e.message)
77
79
  rescue => e
78
80
  debug [:error, e]
79
- # These are application errors - raise unless onerror defined
80
- trigger_on_error(e) || raise(e)
81
+
81
82
  # There is no code defined for application errors, so use 3000
82
83
  # (which is reserved for frameworks)
83
- close_websocket_private(3000)
84
+ close_websocket_private(3000, "Application error")
85
+
86
+ # These are application errors - raise unless onerror defined
87
+ trigger_on_error(e) || raise(e)
84
88
  end
85
89
 
86
90
  def unbind
@@ -219,6 +223,14 @@ module EventMachine
219
223
  end
220
224
  end
221
225
 
226
+ def supports_close_codes?
227
+ if @handler
228
+ @handler.supports_close_codes?
229
+ else
230
+ raise WebSocketError, "Cannot test before onopen callback"
231
+ end
232
+ end
233
+
222
234
  def state
223
235
  @handler ? @handler.state : :handshake
224
236
  end
@@ -232,7 +244,7 @@ module EventMachine
232
244
  # correct close code (1009) immediately after receiving the frame header
233
245
  #
234
246
  def max_frame_size
235
- @max_frame_size || WebSocket.max_frame_size
247
+ defined?(@max_frame_size) ? @max_frame_size : WebSocket.max_frame_size
236
248
  end
237
249
 
238
250
  private
@@ -243,7 +255,7 @@ module EventMachine
243
255
  close_connection
244
256
  end
245
257
 
246
- def close_websocket_private(code, body = nil)
258
+ def close_websocket_private(code, body)
247
259
  if @handler
248
260
  debug [:closing, code]
249
261
  @handler.close_websocket(code, body)
@@ -252,6 +264,19 @@ module EventMachine
252
264
  abort
253
265
  end
254
266
  end
267
+
268
+ # Accept 1000, 3xxx or 4xxx
269
+ #
270
+ # This is consistent with the spec and what browsers have implemented
271
+ # Frameworks should use 3xxx while applications should use 4xxx
272
+ def acceptable_close_code?(code)
273
+ case code
274
+ when 1000, (3000..4999)
275
+ true
276
+ else
277
+ false
278
+ end
279
+ end
255
280
  end
256
281
  end
257
282
  end
@@ -6,6 +6,7 @@ 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
12
  def process_data(newdata)
@@ -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,6 +6,7 @@ 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
12
  def process_data(newdata)
@@ -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,6 +7,7 @@ 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
13
  def process_data(newdata)
@@ -162,7 +163,7 @@ module EventMachine
162
163
  end
163
164
 
164
165
  def opcode_to_type(opcode)
165
- FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode")
166
+ FRAME_TYPES_INVERSE[opcode] || raise(WSProtocolError, "Unknown opcode #{opcode}")
166
167
  end
167
168
 
168
169
  def data_frame?(type)
@@ -2,7 +2,7 @@ module EventMachine
2
2
  module WebSocket
3
3
  class Handler
4
4
  def self.klass_factory(version)
5
- handler_klass = case version
5
+ case version
6
6
  when 75
7
7
  Handler75
8
8
  when 76
@@ -52,7 +52,13 @@ module EventMachine
52
52
 
53
53
  def unbind
54
54
  @state = :closed
55
- @connection.trigger_on_close
55
+
56
+ @close_info = defined?(@close_info) ? @close_info : {
57
+ :code => 1006,
58
+ :was_clean => false,
59
+ }
60
+
61
+ @connection.trigger_on_close(@close_info )
56
62
  end
57
63
 
58
64
  def ping
@@ -1,3 +1,5 @@
1
+ # encoding: BINARY
2
+
1
3
  module EventMachine
2
4
  module WebSocket
3
5
  class Handler76 < Handler
@@ -24,7 +24,7 @@ module EventMachine
24
24
  def receive_data(data)
25
25
  @parser << data
26
26
 
27
- if @headers
27
+ if defined? @headers
28
28
  process(@headers, @parser.upgrade_data)
29
29
  end
30
30
  rescue HTTP::Parser::Error => e
@@ -66,6 +66,10 @@ module EventMachine
66
66
  @headers["origin"] || @headers["sec-websocket-origin"] || nil
67
67
  end
68
68
 
69
+ def secure?
70
+ @secure
71
+ end
72
+
69
73
  private
70
74
 
71
75
  def process(headers, remains)
@@ -10,11 +10,6 @@ module EventMachine
10
10
  raise HandshakeError, "sec-websocket-key header is required"
11
11
  end
12
12
 
13
- # Optional
14
- origin = headers['sec-websocket-origin']
15
- protocols = headers['sec-websocket-protocol']
16
- extensions = headers['sec-websocket-extensions']
17
-
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
 
@@ -15,12 +15,8 @@ module EventMachine
15
15
  @masking_key = nil
16
16
  end
17
17
 
18
- def slice_mask
19
- slice!(0, 4)
20
- end
21
-
22
18
  def getbyte(index)
23
- if @masking_key
19
+ if defined?(@masking_key) && @masking_key
24
20
  masked_char = super
25
21
  masked_char ? masked_char ^ @masking_key.getbyte(index % 4) : nil
26
22
  else
@@ -6,17 +6,20 @@ module EventMachine
6
6
  def message(message_type, extension_data, application_data)
7
7
  case message_type
8
8
  when :close
9
+ @close_info = {
10
+ :code => 1005,
11
+ :reason => "",
12
+ :was_clean => true,
13
+ }
9
14
  if @state == :closing
10
15
  # TODO: Check that message body matches sent data
11
16
  # We can close connection immediately since there is no more data
12
17
  # is allowed to be sent or received on this connection
13
18
  @connection.close_connection
14
- @state = :closed
15
19
  else
16
20
  # Acknowlege close
17
21
  # The connection is considered closed
18
22
  send_frame(:close, application_data)
19
- @state = :closed
20
23
  @connection.close_connection_after_writing
21
24
  end
22
25
  when :ping
@@ -19,18 +19,22 @@ module EventMachine
19
19
 
20
20
  debug [:close_frame_received, status_code, application_data]
21
21
 
22
+ @close_info = {
23
+ :code => status_code || 1005,
24
+ :reason => application_data,
25
+ :was_clean => true,
26
+ }
27
+
22
28
  if @state == :closing
23
29
  # We can close connection immediately since no more data may be
24
30
  # sent or received on this connection
25
31
  @connection.close_connection
26
- @state = :closed
27
- else
28
- # Acknowlege close
32
+ elsif @state == :connected
33
+ # Acknowlege close & echo status back to client
29
34
  # The connection is considered closed
30
- send_frame(:close, '')
31
- @state = :closed
35
+ close_data = [status_code || 1000].pack('n')
36
+ send_frame(:close, close_data)
32
37
  @connection.close_connection_after_writing
33
- # TODO: Send close status code and body to app code
34
38
  end
35
39
  when :ping
36
40
  # Pong back the same data
@@ -41,6 +45,9 @@ module EventMachine
41
45
  when :text
42
46
  if application_data.respond_to?(:force_encoding)
43
47
  application_data.force_encoding("UTF-8")
48
+ unless application_data.valid_encoding?
49
+ raise InvalidDataError, "Invalid UTF8 data"
50
+ end
44
51
  end
45
52
  @connection.trigger_on_message(application_data)
46
53
  when :binary