ably 0.8.5 → 0.8.6

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +42 -48
  4. data/SPEC.md +1099 -640
  5. data/ably.gemspec +10 -4
  6. data/lib/ably/auth.rb +155 -47
  7. data/lib/ably/exceptions.rb +2 -0
  8. data/lib/ably/models/channel_state_change.rb +2 -3
  9. data/lib/ably/models/connection_details.rb +54 -0
  10. data/lib/ably/models/protocol_message.rb +14 -4
  11. data/lib/ably/models/token_details.rb +13 -7
  12. data/lib/ably/models/token_request.rb +1 -2
  13. data/lib/ably/modules/ably.rb +3 -2
  14. data/lib/ably/modules/message_emitter.rb +1 -3
  15. data/lib/ably/modules/state_emitter.rb +2 -2
  16. data/lib/ably/realtime/auth.rb +6 -0
  17. data/lib/ably/realtime/channel/channel_manager.rb +2 -0
  18. data/lib/ably/realtime/channel.rb +15 -4
  19. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  20. data/lib/ably/realtime/client.rb +10 -3
  21. data/lib/ably/realtime/connection/connection_manager.rb +58 -54
  22. data/lib/ably/realtime/connection.rb +62 -6
  23. data/lib/ably/realtime/presence.rb +18 -5
  24. data/lib/ably/rest/channel.rb +9 -1
  25. data/lib/ably/rest/client.rb +32 -14
  26. data/lib/ably/rest/presence.rb +1 -1
  27. data/lib/ably/version.rb +1 -1
  28. data/lib/ably.rb +2 -0
  29. data/spec/acceptance/realtime/auth_spec.rb +251 -11
  30. data/spec/acceptance/realtime/channel_history_spec.rb +12 -2
  31. data/spec/acceptance/realtime/channel_spec.rb +316 -24
  32. data/spec/acceptance/realtime/client_spec.rb +93 -1
  33. data/spec/acceptance/realtime/connection_failures_spec.rb +177 -86
  34. data/spec/acceptance/realtime/connection_spec.rb +284 -60
  35. data/spec/acceptance/realtime/message_spec.rb +45 -6
  36. data/spec/acceptance/realtime/presence_history_spec.rb +4 -0
  37. data/spec/acceptance/realtime/presence_spec.rb +181 -49
  38. data/spec/acceptance/realtime/time_spec.rb +13 -0
  39. data/spec/acceptance/rest/auth_spec.rb +222 -4
  40. data/spec/acceptance/rest/channel_spec.rb +132 -1
  41. data/spec/acceptance/rest/client_spec.rb +129 -28
  42. data/spec/acceptance/rest/presence_spec.rb +7 -7
  43. data/spec/acceptance/rest/time_spec.rb +10 -0
  44. data/spec/shared/client_initializer_behaviour.rb +41 -17
  45. data/spec/spec_helper.rb +1 -0
  46. data/spec/support/debug_failure_helper.rb +16 -0
  47. data/spec/unit/models/connection_details_spec.rb +60 -0
  48. data/spec/unit/models/protocol_message_spec.rb +45 -0
  49. data/spec/unit/modules/event_emitter_spec.rb +3 -1
  50. data/spec/unit/realtime/channel_spec.rb +6 -5
  51. data/spec/unit/realtime/client_spec.rb +5 -1
  52. data/spec/unit/realtime/connection_spec.rb +5 -1
  53. data/spec/unit/realtime/realtime_spec.rb +5 -1
  54. metadata +54 -7
data/ably.gemspec CHANGED
@@ -8,10 +8,10 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Ably::VERSION
9
9
  spec.authors = ['Lewis Marshall', "Matthew O'Riordan"]
10
10
  spec.email = ['lewis@lmars.net', 'matt@ably.io']
11
- spec.description = %q{A Ruby client library for ably.io, the realtime messaging service}
12
- spec.summary = %q{A Ruby client library for ably.io, the realtime messaging service}
11
+ spec.description = %q{A Ruby client library for ably.io realtime messaging}
12
+ spec.summary = %q{A Ruby client library for ably.io realtime messaging implemented using EventMachine}
13
13
  spec.homepage = 'http://github.com/ably/ably-ruby'
14
- spec.license = 'MIT'
14
+ spec.license = 'Apache 2'
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
@@ -25,14 +25,20 @@ Gem::Specification.new do |spec|
25
25
  spec.add_runtime_dependency 'json'
26
26
  spec.add_runtime_dependency 'websocket-driver', '~> 0.3'
27
27
  spec.add_runtime_dependency 'msgpack', '>= 0.6.2'
28
+ spec.add_runtime_dependency 'addressable', '>= 2.0.0'
28
29
 
29
30
  spec.add_development_dependency 'bundler', '~> 1.3'
30
31
  spec.add_development_dependency 'rake'
31
32
  spec.add_development_dependency 'redcarpet'
32
- spec.add_development_dependency 'rspec', '~> 3.1.0' # version lock, see config.around(:example, :event_machine) in event_machine_helper.rb
33
+ spec.add_development_dependency 'rspec', '~> 3.2.0' # version lock, see config.around(:example, :event_machine) in event_machine_helper.rb
33
34
  spec.add_development_dependency 'rspec-retry'
34
35
  spec.add_development_dependency 'yard'
35
36
  spec.add_development_dependency 'webmock'
36
37
 
37
38
  spec.add_development_dependency 'coveralls'
39
+
40
+ if RUBY_VERSION.match(/^2/)
41
+ spec.add_development_dependency 'pry'
42
+ spec.add_development_dependency 'pry-byebug'
43
+ end
38
44
  end
data/lib/ably/auth.rb CHANGED
@@ -13,8 +13,6 @@ module Ably
13
13
  # @return [String] The provided client ID, used for identifying this client for presence purposes
14
14
  # @!attribute [r] current_token_details
15
15
  # @return [Ably::Models::TokenDetails] Current {Ably::Models::TokenDetails} issued by this library or one of the provided callbacks used to authenticate requests
16
- # @!attribute [r] token
17
- # @return [String] Token string provided to the {Ably::Client} constructor that is used to authenticate all requests
18
16
  # @!attribute [r] key
19
17
  # @return [String] Complete API key containing both the key name and key secret, if present
20
18
  # @!attribute [r] key_name
@@ -61,6 +59,7 @@ module Ably
61
59
  @client = client
62
60
  @options = auth_options.dup
63
61
  @token_params = token_params.dup
62
+ @token_option = options[:token] || options[:token_details]
64
63
 
65
64
  @options.delete :force # Forcing token auth for every request is not a valid default
66
65
 
@@ -74,11 +73,25 @@ module Ably
74
73
  raise ArgumentError, 'key is missing. Either an API key, token, or token auth method must be provided'
75
74
  end
76
75
 
77
- if has_client_id? && !token_creatable_externally?
78
- raise ArgumentError, 'client_id cannot be provided without a complete API key. Key name & Secret is needed to authenticate with Ably and obtain a token' unless api_key_present?
76
+ if options[:client_id] == '*'
77
+ raise ArgumentError, 'A client cannot be configured with a wildcard client_id'
78
+ end
79
+
80
+ if has_client_id? && !token_creatable_externally? && !token_option
81
+ raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present?
79
82
  ensure_utf_8 :client_id, client_id
80
83
  end
81
84
 
85
+ # If a token details object or token string is provided in the initializer
86
+ # then the client can be authorised immediately using this token
87
+ if token_option
88
+ token_details = convert_to_token_details(token_option)
89
+ if token_details
90
+ token_details = authorise_with_token(token_details)
91
+ logger.debug "Auth: new token passed in to the initializer: #{token_details}"
92
+ end
93
+ end
94
+
82
95
  @options.freeze
83
96
  @token_params.freeze
84
97
  end
@@ -120,7 +133,9 @@ module Ably
120
133
  token_params = (auth_options.delete(:token_params) || {}).merge(token_params)
121
134
  @token_params = @token_params.merge(token_params) # update defaults
122
135
 
123
- @current_token_details = request_token(token_params, auth_options)
136
+ authorise_with_token(request_token(token_params, auth_options)).tap do |new_token_details|
137
+ logger.debug "Auth: new token following authorisation: #{new_token_details}"
138
+ end
124
139
  end
125
140
 
126
141
  # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests
@@ -154,34 +169,29 @@ module Ably
154
169
  def request_token(token_params = {}, auth_options = {})
155
170
  ensure_valid_auth_attributes auth_options
156
171
 
157
- token_params = (auth_options[:token_params] || {}).merge(token_params)
158
- token_params = self.token_params.merge(token_params)
172
+ # Token param precedence (lowest to highest):
173
+ # Auth default => client_id => auth_options[:token_params] arg => token_params arg
174
+ token_params = self.token_params.merge(
175
+ (client_id ? { client_id: client_id } : {}).
176
+ merge(auth_options[:token_params] || {}).
177
+ merge(token_params)
178
+ )
179
+
159
180
  auth_options = self.options.merge(auth_options)
160
181
 
161
182
  token_request = if auth_callback = auth_options.delete(:auth_callback)
162
183
  auth_callback.call(token_params)
163
184
  elsif auth_url = auth_options.delete(:auth_url)
164
- token_request_from_auth_url(auth_url, auth_options)
185
+ token_request_from_auth_url(auth_url, auth_options, token_params)
165
186
  else
166
187
  create_token_request(token_params, auth_options)
167
188
  end
168
189
 
169
- case token_request
170
- when Ably::Models::TokenDetails
171
- return token_request
172
- when Hash
173
- return Ably::Models::TokenDetails.new(token_request) if IdiomaticRubyWrapper(token_request).has_key?(:issued)
174
- when String
175
- return Ably::Models::TokenDetails.new(token: token_request)
190
+ convert_to_token_details(token_request).tap do |token_details|
191
+ return token_details if token_details
176
192
  end
177
193
 
178
- token_request = Ably::Models::TokenRequest(token_request)
179
-
180
- response = client.post("/keys/#{token_request.key_name}/requestToken",
181
- token_request.hash, send_auth_header: false,
182
- disable_automatic_reauthorise: true)
183
-
184
- Ably::Models::TokenDetails.new(response.body)
194
+ send_token_request(token_request)
185
195
  end
186
196
 
187
197
  # Creates and signs a token request that can then subsequently be used by any client to request a token
@@ -277,21 +287,23 @@ module Ably
277
287
  # True when Token Auth is being used to authenticate with Ably
278
288
  def using_token_auth?
279
289
  return options[:use_token_auth] if options.has_key?(:use_token_auth)
280
- !!(token || current_token_details || has_client_id? || token_creatable_externally?)
290
+ !!(token_option || current_token_details || has_client_id? || token_creatable_externally?)
281
291
  end
282
292
 
283
293
  def client_id
284
- options[:client_id]
294
+ @client_id || options[:client_id]
285
295
  end
286
296
 
287
- def token
288
- token_object = options[:token] || options[:token_details]
289
-
290
- if token_object.kind_of?(Ably::Models::TokenDetails)
291
- token_object.token
292
- else
293
- token_object
294
- end
297
+ # When a client has authenticated with Ably and the client is either anonymous (cannot assume a +client_id+)
298
+ # or has an assigned +client_id+ (implicit in all operations), then this client has a validated +client_id+, even
299
+ # if that client_id is +nil+ (anonymous)
300
+ #
301
+ # Once validated by Ably, the client library will enforce the use of the +client_id+ identity provided by Ably, rejecting
302
+ # messages with an invalid +client_id+ immediately
303
+ #
304
+ # @return [Boolean]
305
+ def client_id_validated?
306
+ !!@client_id_validated
295
307
  end
296
308
 
297
309
  # Auth header string used in HTTP requests to Ably
@@ -327,7 +339,7 @@ module Ably
327
339
  #
328
340
  # @return [Boolean]
329
341
  def token_renewable?
330
- token_creatable_externally? || (api_key_present? && !token)
342
+ token_creatable_externally? || (api_key_present? && !token_option)
331
343
  end
332
344
 
333
345
  # Returns false when attempting to send an API Key over a non-secure connection
@@ -338,8 +350,59 @@ module Ably
338
350
  client.use_tls? || using_token_auth?
339
351
  end
340
352
 
353
+ # True if token provided client_id is compatible with the client's configured +client_id+, when applicable
354
+ #
355
+ # @return [Boolean]
356
+ # @api private
357
+ def token_client_id_allowed?(token_client_id)
358
+ return true if client_id.nil? # no explicit client_id specified for this client
359
+ return true if client_id == '*' || token_client_id == '*' # wildcard supported always
360
+ token_client_id == client_id
361
+ end
362
+
363
+ # True if assumed_client_id is compatible with the client's configured or Ably assigned +client_id+
364
+ #
365
+ # @return [Boolean]
366
+ # @api private
367
+ def can_assume_client_id?(assumed_client_id)
368
+ if client_id_validated?
369
+ client_id == '*' || (client_id == assumed_client_id)
370
+ elsif !options[:client_id] || options[:client_id] == '*'
371
+ true # client ID is unknown
372
+ else
373
+ options[:client_id] == assumed_client_id
374
+ end
375
+ end
376
+
377
+ # Configures the client ID for this client
378
+ # Typically this occurs following an Auth or receiving a {Ably::Models::ProtocolMessage} with a +client_id+ in the {Ably::Models::ConnectionDetails}
379
+ #
380
+ # @api private
381
+ def configure_client_id(new_client_id)
382
+ # If new client ID from Ably is a wildcard, but preconfigured clientId is set, then keep the existing clientId
383
+ if has_client_id? && new_client_id == '*'
384
+ @client_id_validated = true
385
+ return
386
+ end
387
+
388
+ # If client_id is defined and not a wildcard, prevent it changing, this is not supported
389
+ if client_id && client_id != '*' && new_client_id != client_id
390
+ raise Ably::Exceptions::IncompatibleClientId.new("Client ID is immutable once configured for a client. Client ID cannot be changed to '#{new_client_id}'", 400, 40012)
391
+ end
392
+ @client_id_validated = true
393
+ @client_id = new_client_id
394
+ end
395
+
396
+ # True when a client_id other than a wildcard is configured for Auth
397
+ #
398
+ # @api private
399
+ def has_client_id?
400
+ client_id && (client_id != '*')
401
+ end
402
+
341
403
  private
342
404
  attr_reader :client
405
+ attr_reader :token_option
343
406
 
344
407
  def ensure_valid_auth_attributes(attributes)
345
408
  if attributes[:timestamp]
@@ -401,17 +464,21 @@ module Ably
401
464
 
402
465
  # Returns the current token if it exists or authorises and retrieves a token
403
466
  def token_auth_string
404
- # If a TokenDetails object has been issued by this library
405
- # then that Token will take precedence
406
- if @current_token_details
407
- authorise.token
408
- elsif token # token string was configured in the options
409
- token
467
+ if !current_token_details && token_option
468
+ # A TokenRequest was configured in the ClientOptions +:token field+ and no current token exists
469
+ # Note: If a Token or TokenDetails is provided in the initializer, the token is stored in +current_token_details+
470
+ authorise_with_token send_token_request(token_option)
471
+ current_token_details.token
410
472
  else
473
+ # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued
411
474
  authorise.token
412
475
  end
413
476
  end
414
477
 
478
+ def configure_current_token_details(token_details)
479
+ @current_token_details = token_details
480
+ end
481
+
415
482
  # Token Auth HTTP Authorization header value
416
483
  def token_auth_header
417
484
  "Bearer #{encode64(token_auth_string)}"
@@ -455,15 +522,20 @@ module Ably
455
522
  # Retrieve a token request from a specified URL, expects a JSON response
456
523
  #
457
524
  # @return [Hash]
458
- def token_request_from_auth_url(auth_url, auth_options)
525
+ def token_request_from_auth_url(auth_url, auth_options, token_params)
459
526
  uri = URI.parse(auth_url)
460
527
  connection = Faraday.new("#{uri.scheme}://#{uri.host}", connection_options)
461
- method = auth_options[:auth_method] || :get
528
+ method = auth_options[:auth_method] || options[:auth_method] || :get
529
+ params = (auth_options[:auth_params] || options[:auth_method] || {}).merge(token_params)
462
530
 
463
531
  response = connection.send(method) do |request|
464
532
  request.url uri.path
465
- request.params = CGI.parse(uri.query || '').merge(auth_options[:auth_params] || {})
466
533
  request.headers = auth_options[:auth_headers] || {}
534
+ if method.to_s.downcase == 'post'
535
+ request.body = params
536
+ else
537
+ request.params = (Addressable::URI.parse(uri.to_s).query_values || {}).merge(params)
538
+ end
467
539
  end
468
540
 
469
541
  if !response.body.kind_of?(Hash) && !response.headers['Content-Type'].to_s.match(%r{text/plain}i)
@@ -474,6 +546,42 @@ module Ably
474
546
  response.body
475
547
  end
476
548
 
549
+ # Use the provided token to authenticate immediately and store the token details in +current_token_details+
550
+ def authorise_with_token(new_token_details)
551
+ if new_token_details && !new_token_details.from_token_string?
552
+ if !token_client_id_allowed?(new_token_details.client_id)
553
+ raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012)
554
+ end
555
+ configure_client_id new_token_details.client_id
556
+ end
557
+ configure_current_token_details new_token_details
558
+ end
559
+
560
+ # Returns a TokenDetails object if the provided token_details_obj argument is a TokenDetails object, Token String
561
+ # or TokenDetails JSON object.
562
+ # If the token_details_obj is not a Token or TokenDetails +nil+ is returned
563
+ def convert_to_token_details(token_details_obj)
564
+ case token_details_obj
565
+ when Ably::Models::TokenDetails
566
+ return token_details_obj
567
+ when Hash
568
+ return Ably::Models::TokenDetails.new(token_details_obj) if IdiomaticRubyWrapper(token_details_obj).has_key?(:issued)
569
+ when String
570
+ return Ably::Models::TokenDetails.new(token: token_details_obj)
571
+ end
572
+ end
573
+
574
+ # @return [Ably::Models::TokenDetails]
575
+ def send_token_request(token_request)
576
+ token_request = Ably::Models::TokenRequest(token_request)
577
+
578
+ response = client.post("/keys/#{token_request.key_name}/requestToken",
579
+ token_request.hash, send_auth_header: false,
580
+ disable_automatic_reauthorise: true)
581
+
582
+ Ably::Models::TokenDetails.new(response.body)
583
+ end
584
+
477
585
  # Return a Hash of connection options to initiate the Faraday::Connection with
478
586
  #
479
587
  # @return [Hash]
@@ -501,7 +609,7 @@ module Ably
501
609
  # Raise exceptions if response code is invalid
502
610
  builder.use Ably::Rest::Middleware::ExternalExceptions
503
611
 
504
- setup_incoming_middleware builder, client.logger
612
+ setup_incoming_middleware builder, logger
505
613
 
506
614
  # Set Faraday's HTTP adapter
507
615
  builder.adapter Faraday.default_adapter
@@ -520,12 +628,12 @@ module Ably
520
628
  auth_callback_present? || token_url_present?
521
629
  end
522
630
 
523
- def has_client_id?
524
- !!client_id
525
- end
526
-
527
631
  def api_key_present?
528
632
  key_name && key_secret
529
633
  end
634
+
635
+ def logger
636
+ client.logger
637
+ end
530
638
  end
531
639
  end
@@ -106,5 +106,7 @@ module Ably
106
106
 
107
107
  # When a channel is detached / failed, certain operations are not permitted such as publishing messages
108
108
  class ChannelInactive < BaseAblyException; end
109
+
110
+ class IncompatibleClientId < BaseAblyException; end
109
111
  end
110
112
  end
@@ -3,9 +3,9 @@ module Ably::Models
3
3
  # when a state change occurs
4
4
  #
5
5
  # @!attribute [r] current
6
- # @return [Connection::STATE] Current connection state
6
+ # @return [Connection::STATE] Current channel state
7
7
  # @!attribute [r] previous
8
- # @return [Connection::STATE] Previous connection state
8
+ # @return [Connection::STATE] Previous channel state
9
9
  # @!attribute [r] reason
10
10
  # @return [Ably::Models::ErrorInfo] Object describing the reason for a state change when not initiated by the consumer of the client library
11
11
  #
@@ -20,7 +20,6 @@ module Ably::Models
20
20
  @hash_object = {
21
21
  current: hash_object.fetch(:current),
22
22
  previous: hash_object.fetch(:previous),
23
- retry_in: hash_object[:retry_in],
24
23
  reason: hash_object[:reason],
25
24
  protocol_message: hash_object[:protocol_message]
26
25
  }
@@ -0,0 +1,54 @@
1
+ module Ably::Models
2
+ # Convert connection details attributes to a {ConnectionDetails} object
3
+ #
4
+ # @param attributes (see #initialize)
5
+ #
6
+ # @return [ConnectionDetails]
7
+ def self.ConnectionDetails(attributes)
8
+ case attributes
9
+ when ConnectionDetails
10
+ return attributes
11
+ else
12
+ ConnectionDetails.new(attributes || {})
13
+ end
14
+ end
15
+
16
+ # ConnectionDetails are optionally passed to the client library in the +CONNECTED+ {Ably::Models::ProtocolMessage#connectionDetails} attribute
17
+ # to inform the client about any constraints it should adhere to and provide additional metadata about the connection.
18
+ # For example, if a request is made to publish a message that exceeds the +maxMessageSize+, the client library can reject
19
+ # the message immediately, without communicating with the Ably service
20
+ #
21
+ class ConnectionDetails
22
+ include Ably::Modules::ModelCommon
23
+
24
+ # @param attributes [Hash]
25
+ # @option attributes [String] :client_id contains the client ID assigned to the connection
26
+ # @option attributes [String] :connection_key the connection secret key string that is used to resume a connection and its state
27
+ # @option attributes [Integer] :max_message_size maximum individual message size in bytes
28
+ # @option attributes [Integer] :max_frame_size maximum size for a single frame of data sent to Ably. This restriction applies to a {Ably::Models::ProtocolMessage} sent over a realtime connection, or the total body size for a REST request
29
+ # @option attributes [Integer] :max_inbound_rate maximum allowable number of requests per second from a client
30
+ # @option attributes [Integer] :connection_state_ttl duration in seconds that Ably will persist the connection state when a Realtime client is abruptly disconnected
31
+ #
32
+ def initialize(attributes = {})
33
+ @hash_object = IdiomaticRubyWrapper(attributes.clone)
34
+ hash[:connection_state_ttl] = (hash[:connection_state_ttl].to_f / 1000).round if hash[:connection_state_ttl]
35
+ hash.freeze
36
+ end
37
+
38
+ %w(client_id connection_key max_message_size max_frame_size max_inbound_rate connection_state_ttl).each do |attribute|
39
+ define_method attribute do
40
+ hash[attribute.to_sym]
41
+ end
42
+ end
43
+
44
+ def has_client_id?
45
+ hash.has_key?(:client_id)
46
+ end
47
+
48
+ # @!attribute [r] hash
49
+ # @return [Hash] Access the token details Hash object ruby'fied to use symbolized keys
50
+ def hash
51
+ @hash_object
52
+ end
53
+ end
54
+ end
@@ -10,12 +10,12 @@ module Ably::Models
10
10
  # @return [ACTION] Protocol Message action {Ably::Modules::Enum} from list of {ACTION}. Returns nil if action is unsupported by protocol
11
11
  # @!attribute [r] count
12
12
  # @return [Integer] The count field is used for ACK and NACK actions. See {http://docs.ably.io/client-lib-development-guide/protocol/#message-acknowledgement message acknowledgement protocol}
13
- # @!attribute [r] error_info
13
+ # @!attribute [r] error
14
14
  # @return [ErrorInfo] Contains error information
15
15
  # @!attribute [r] channel
16
16
  # @return [String] Channel name for messages
17
17
  # @!attribute [r] channel_serial
18
- # @return [String] Contains a serial number for a message on the current channel
18
+ # @return [String] Contains a serial number for a message on the current channelƒ
19
19
  # @!attribute [r] connection_id
20
20
  # @return [String] Contains a string public identifier for the connection
21
21
  # @!attribute [r] connection_key
@@ -88,12 +88,18 @@ module Ably::Models
88
88
  @hash_object.freeze
89
89
  end
90
90
 
91
- %w(id channel channel_serial connection_id connection_key).each do |attribute|
91
+ %w(id channel channel_serial connection_id).each do |attribute|
92
92
  define_method attribute do
93
93
  hash[attribute.to_sym]
94
94
  end
95
95
  end
96
96
 
97
+ def connection_key
98
+ # connection_key in connection details takes precedence over connection_key on the ProtocolMessage
99
+ # connection_key in the ProtocolMessage will be deprecated in future protocol versions > 0.8
100
+ connection_details.connection_key || hash[:connection_key]
101
+ end
102
+
97
103
  def id!
98
104
  raise RuntimeError, 'ProtocolMessage #id is nil' unless id
99
105
  id
@@ -106,7 +112,7 @@ module Ably::Models
106
112
  end
107
113
 
108
114
  def error
109
- @error_info ||= ErrorInfo.new(hash[:error]) if hash[:error]
115
+ @error ||= ErrorInfo.new(hash[:error]) if hash[:error]
110
116
  end
111
117
 
112
118
  def timestamp
@@ -181,6 +187,10 @@ module Ably::Models
181
187
  flags & 1 == 1
182
188
  end
183
189
 
190
+ def connection_details
191
+ @connection_details ||= Ably::Models::ConnectionDetails(hash[:connection_details])
192
+ end
193
+
184
194
  # Indicates this protocol message will generate an ACK response when sent
185
195
  # Examples of protocol messages required ACK include :message and :presence
186
196
  def ack_required?
@@ -1,8 +1,7 @@
1
1
  module Ably::Models
2
2
  # Convert token details argument to a {TokenDetails} object
3
3
  #
4
- # @param attributes [TokenDetails,Hash] A {TokenDetails} object or Hash of token and meta data attributes
5
- # @option attributes (see TokenDetails#initialize)
4
+ # @param attributes (see #initialize)
6
5
  #
7
6
  # @return [TokenDetails]
8
7
  def self.TokenDetails(attributes)
@@ -27,10 +26,6 @@ module Ably::Models
27
26
  # For example, if buffer is 10s, the token can no longer be used for new requests 9s before it expires
28
27
  TOKEN_EXPIRY_BUFFER = 15
29
28
 
30
- def initialize(attributes)
31
- @hash_object = IdiomaticRubyWrapper(attributes.clone.freeze)
32
- end
33
-
34
29
  # @param attributes
35
30
  # @option attributes [String] :token token used to authenticate requests
36
31
  # @option attributes [String] :key_name API key name used to create this token
@@ -76,7 +71,7 @@ module Ably::Models
76
71
  # @!attribute [r] capability
77
72
  # @return [Hash] Capabilities assigned to this token
78
73
  def capability
79
- JSON.parse(hash.fetch(:capability)) if hash.fetch(:capability)
74
+ JSON.parse(hash.fetch(:capability)) if hash.has_key?(:capability)
80
75
  end
81
76
 
82
77
  # @!attribute [r] client_id
@@ -94,10 +89,21 @@ module Ably::Models
94
89
  expires < Time.now + TOKEN_EXPIRY_BUFFER
95
90
  end
96
91
 
92
+ # True if the TokenDetails was created from an opaque string i.e. no metadata exists for this token
93
+ # @return [Boolean]
94
+ # @api private
95
+ def from_token_string?
96
+ hash.keys == [:token]
97
+ end
98
+
97
99
  # @!attribute [r] hash
98
100
  # @return [Hash] Access the token details Hash object ruby'fied to use symbolized keys
99
101
  def hash
100
102
  @hash_object
101
103
  end
104
+
105
+ def to_s
106
+ "<TokenDetails token=#{token} client_id=#{client_id} key_name=#{key_name} issued=#{issued} expires=#{expires} capability=#{capability} expired?=#{expired?}>"
107
+ end
102
108
  end
103
109
  end
@@ -1,8 +1,7 @@
1
1
  module Ably::Models
2
2
  # Convert token request argument to a {TokenRequest} object
3
3
  #
4
- # @param attributes [TokenRequest,Hash] A {TokenRequest} object or Hash of attributes to create a new token request
5
- # @option attributes (see TokenRequest#initialize)
4
+ # @param attributes (see #initialize)
6
5
  #
7
6
  # @return [TokenRequest]
8
7
  def self.TokenRequest(attributes)
@@ -7,9 +7,10 @@ module Ably
7
7
  # Fallback hosts to use when a connection to rest/realtime.ably.io is not possible due to
8
8
  # network failures either at the client, between the client and Ably, within an Ably data center, or at the IO domain registrar
9
9
  #
10
- FALLBACK_HOSTS = %w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com)
10
+ FALLBACK_HOSTS = %w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com).freeze
11
+
11
12
  INTERNET_CHECK = {
12
13
  url: '//internet-up.ably-realtime.com/is-the-internet-up.txt',
13
14
  ok_text: 'yes'
14
- }
15
+ }.freeze
15
16
  end
@@ -50,10 +50,8 @@ module Ably::Modules
50
50
  #
51
51
  # @api private
52
52
  def emit_message(name, payload)
53
- raise 'Event name is required' unless name
54
-
55
53
  message_emitter_subscriptions[:all].each { |cb| cb.call(payload) }
56
- message_emitter_subscriptions[name].each { |cb| cb.call(payload) }
54
+ message_emitter_subscriptions[name].each { |cb| cb.call(payload) } if name
57
55
  end
58
56
 
59
57
  private
@@ -71,11 +71,11 @@ module Ably::Modules
71
71
  # @yield block is called if the state is matched immediately or once when the state is reached
72
72
  #
73
73
  # @return [void]
74
- def once_or_if(target_states, options = {})
74
+ def once_or_if(target_states, options = {}, &block)
75
75
  raise ArgumentError, 'Block required' unless block_given?
76
76
 
77
77
  if Array(target_states).any? { |target_state| state == target_state }
78
- yield
78
+ safe_yield block
79
79
  else
80
80
  failure_block = options.fetch(:else, nil)
81
81
  failure_wrapper = nil
@@ -37,6 +37,8 @@ module Ably
37
37
  include Ably::Modules::AsyncWrapper
38
38
 
39
39
  def_delegators :auth_sync, :client_id
40
+ def_delegators :auth_sync, :token_client_id_allowed?, :configure_client_id, :client_id_validated?
41
+ def_delegators :auth_sync, :can_assume_client_id?, :has_client_id?
40
42
  def_delegators :auth_sync, :current_token_details, :token
41
43
  def_delegators :auth_sync, :key, :key_name, :key_secret, :options, :auth_options, :token_params
42
44
  def_delegators :auth_sync, :using_basic_auth?, :using_token_auth?
@@ -68,6 +70,10 @@ module Ably
68
70
  def authorise(token_params = {}, auth_options = {}, &success_callback)
69
71
  async_wrap(success_callback) do
70
72
  auth_sync.authorise(token_params, auth_options)
73
+ end.tap do |deferrable|
74
+ deferrable.errback do |error|
75
+ client.connection.transition_state_machine :failed, reason: error if error.kind_of?(Ably::Exceptions::IncompatibleClientId)
76
+ end
71
77
  end
72
78
  end
73
79
 
@@ -123,6 +123,8 @@ module Ably::Realtime
123
123
  # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
124
124
  # base on the serial numbers
125
125
  #
126
+ # TODO: Move this into the Connection class, it does not belong in a Channel class
127
+ #
126
128
  # @api private
127
129
  def resend_pending_message_ack_queue
128
130
  connection.__pending_message_ack_queue__.delete_if do |protocol_message|
@@ -309,9 +309,20 @@ module Ably
309
309
 
310
310
  # Queue messages and process queue if channel is attached.
311
311
  # If channel is not yet attached, attempt to attach it before the message queue is processed.
312
- # @returns [Ably::Util::SafeDeferrable]
312
+ # @return [Ably::Util::SafeDeferrable]
313
313
  def queue_messages(raw_messages)
314
- messages = Array(raw_messages).map { |msg| create_message(msg) }
314
+ messages = Array(raw_messages).map do |raw_msg|
315
+ create_message(raw_msg).tap do |message|
316
+ next if message.client_id.nil?
317
+ if message.client_id == '*'
318
+ raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages', 400, 40012)
319
+ end
320
+ unless client.auth.can_assume_client_id?(message.client_id)
321
+ raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{message.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'", 400, 40012)
322
+ end
323
+ end
324
+ end
325
+
315
326
  queue.push *messages
316
327
 
317
328
  if attached?
@@ -340,12 +351,12 @@ module Ably
340
351
  Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
341
352
  messages.each do |message|
342
353
  message.callback do
343
- return if failed
354
+ next if failed
344
355
  actual_deliveries += 1
345
356
  deferrable.succeed messages if actual_deliveries == expected_deliveries
346
357
  end
347
358
  message.errback do |error|
348
- return if failed
359
+ next if failed
349
360
  failed = true
350
361
  deferrable.fail error, message
351
362
  end