ably 0.8.8 → 0.8.9

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -2
  3. data/LICENSE +2 -2
  4. data/README.md +81 -20
  5. data/SPEC.md +235 -178
  6. data/lib/ably/auth.rb +1 -1
  7. data/lib/ably/exceptions.rb +10 -1
  8. data/lib/ably/models/cipher_params.rb +114 -0
  9. data/lib/ably/models/connection_details.rb +8 -6
  10. data/lib/ably/models/error_info.rb +3 -3
  11. data/lib/ably/models/idiomatic_ruby_wrapper.rb +27 -20
  12. data/lib/ably/models/message.rb +15 -15
  13. data/lib/ably/models/message_encoders/cipher.rb +8 -7
  14. data/lib/ably/models/presence_message.rb +17 -17
  15. data/lib/ably/models/protocol_message.rb +26 -19
  16. data/lib/ably/models/stats.rb +15 -15
  17. data/lib/ably/models/token_details.rb +14 -12
  18. data/lib/ably/models/token_request.rb +16 -14
  19. data/lib/ably/modules/async_wrapper.rb +1 -1
  20. data/lib/ably/modules/encodeable.rb +10 -10
  21. data/lib/ably/modules/model_common.rb +13 -5
  22. data/lib/ably/realtime/channel.rb +1 -2
  23. data/lib/ably/realtime/presence.rb +29 -58
  24. data/lib/ably/realtime/presence/members_map.rb +2 -2
  25. data/lib/ably/rest/channel.rb +1 -2
  26. data/lib/ably/rest/middleware/exceptions.rb +14 -4
  27. data/lib/ably/rest/presence.rb +3 -1
  28. data/lib/ably/util/crypto.rb +50 -40
  29. data/lib/ably/version.rb +1 -1
  30. data/spec/acceptance/realtime/message_spec.rb +20 -20
  31. data/spec/acceptance/realtime/presence_history_spec.rb +7 -7
  32. data/spec/acceptance/realtime/presence_spec.rb +65 -77
  33. data/spec/acceptance/rest/auth_spec.rb +8 -8
  34. data/spec/acceptance/rest/base_spec.rb +4 -4
  35. data/spec/acceptance/rest/channel_spec.rb +1 -1
  36. data/spec/acceptance/rest/client_spec.rb +1 -1
  37. data/spec/acceptance/rest/encoders_spec.rb +4 -4
  38. data/spec/acceptance/rest/message_spec.rb +15 -15
  39. data/spec/acceptance/rest/presence_spec.rb +4 -4
  40. data/spec/shared/model_behaviour.rb +7 -7
  41. data/spec/unit/models/cipher_params_spec.rb +140 -0
  42. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +15 -8
  43. data/spec/unit/models/message_encoders/cipher_spec.rb +28 -22
  44. data/spec/unit/models/message_encoders/json_spec.rb +24 -0
  45. data/spec/unit/models/protocol_message_spec.rb +3 -3
  46. data/spec/unit/util/crypto_spec.rb +50 -17
  47. metadata +5 -2
@@ -585,7 +585,7 @@ module Ably
585
585
  token_request = Ably::Models::TokenRequest(token_request)
586
586
 
587
587
  response = client.post("/keys/#{token_request.key_name}/requestToken",
588
- token_request.hash, send_auth_header: false,
588
+ token_request.attributes, send_auth_header: false,
589
589
  disable_automatic_reauthorise: true)
590
590
 
591
591
  Ably::Models::TokenDetails.new(response.body)
@@ -32,6 +32,15 @@ module Ably
32
32
  # An invalid request was received by Ably
33
33
  class InvalidRequest < BaseAblyException; end
34
34
 
35
+ # Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided
36
+ class UnauthorizedRequest < BaseAblyException; end
37
+
38
+ # The request was a valid request, but Ably is refusing to respond to it
39
+ class ForbiddenRequest < BaseAblyException; end
40
+
41
+ # The requested resource could not be found but may be available again in the future
42
+ class ResourceMissing < BaseAblyException; end
43
+
35
44
  # Ably Protocol message received that is invalid
36
45
  class ProtocolError < BaseAblyException; end
37
46
 
@@ -92,7 +101,7 @@ module Ably
92
101
  # The token request could not be created
93
102
  class TokenRequestFailed < BaseAblyException; end
94
103
 
95
- # The token has expired
104
+ # The token has expired, 40140..40149
96
105
  class TokenExpired < BaseAblyException; end
97
106
 
98
107
  # The message could not be delivered to the server
@@ -0,0 +1,114 @@
1
+ require 'base64'
2
+ require 'ably/util/crypto'
3
+
4
+ module Ably::Models
5
+ # Convert cipher param attributes to a {CipherParams} object
6
+ #
7
+ # @param attributes (see #initialize)
8
+ #
9
+ # @return [CipherParams]
10
+ def self.CipherParams(attributes)
11
+ case attributes
12
+ when CipherParams
13
+ return attributes
14
+ else
15
+ CipherParams.new(attributes || {})
16
+ end
17
+ end
18
+
19
+ # CipherParams is used to configure a channel for encryption
20
+ #
21
+ class CipherParams
22
+ include Ably::Modules::ModelCommon
23
+
24
+ # @param params [Hash]
25
+ # @option params [String,Binary] :key Required private key must be either a binary (e.g. a ASCII_8BIT encoded string), or a base64-encoded string. If the key is a base64-encoded string, the it will be automatically converted into a binary
26
+ # @option params [String] :algorithm optional (default AES), specify the encryption algorithm supported by {http://ruby-doc.org/stdlib-2.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html OpenSSL::Cipher}
27
+ # @option params [String] :mode optional (default CBC), specify the cipher mode supported by {http://ruby-doc.org/stdlib-2.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html OpenSSL::Cipher}
28
+ # @option params [Integer] :key_length optional (default 128), specify the key length of the cipher supported by {http://ruby-doc.org/stdlib-2.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html OpenSSL::Cipher}
29
+ # @option params [String] :combined optional (default AES-128-CBC), specify in one option the algorithm, key length and cipher of the cipher supported by {http://ruby-doc.org/stdlib-2.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html OpenSSL::Cipher}
30
+ #
31
+ def initialize(params = {})
32
+ @attributes = IdiomaticRubyWrapper(params.clone)
33
+
34
+ raise Ably::Exceptions::CipherError, ':key param is required' unless attributes[:key]
35
+ raise Ably::Exceptions::CipherError, ':key param must be a base64-encoded string or byte array (ASCII_8BIT enocdede string)' unless key.kind_of?(String)
36
+ attributes[:key] = decode_key(key) if key.kind_of?(String) && key.encoding != Encoding::ASCII_8BIT
37
+
38
+ if attributes[:combined]
39
+ match = /(?<algorithm>\w+)-(?<key_length>\d+)-(?<mode>\w+)/.match(attributes[:combined])
40
+ raise Ably::Exceptions::CipherError, "Invalid :combined param, expecting format such as AES-256-CBC" unless match
41
+ attributes[:algorithm] = match[:algorithm]
42
+ attributes[:key_length] = match[:key_length].to_i
43
+ attributes[:mode] = match[:mode]
44
+ end
45
+
46
+ if attributes[:key_length] && (key_length != attributes[:key_length])
47
+ raise Ably::Exceptions::CipherError, "Incompatible :key length of #{key_length} and provided :key_length of #{attributes[:key_length]}"
48
+ end
49
+
50
+ if algorithm == 'aes' && mode == 'cbc'
51
+ unless [128, 256].include?(key_length)
52
+ raise Ably::Exceptions::CipherError, "Unsupported key length #{key_length} for aes-cbc encryption. Encryption key must be 128 or 256 bits (16 or 32 ASCII characters)"
53
+ end
54
+ end
55
+
56
+ attributes.freeze
57
+ end
58
+
59
+ # The Cipher algorithm string such as AES-128-CBC
60
+ # @param [Hash] params Hash containing :algorithm, :key_length and :mode key values
61
+ #
62
+ # @return [String]
63
+ def self.cipher_type(params)
64
+ "#{params[:algorithm]}-#{params[:key_length]}-#{params[:mode]}".to_s.upcase
65
+ end
66
+
67
+ # @!attribute [r] algorithm
68
+ # @return [String] The algorithm to use for encryption, currently only +AES+ is supported
69
+ def algorithm
70
+ attributes.fetch(:algorithm) do
71
+ Ably::Util::Crypto::DEFAULTS.fetch(:algorithm)
72
+ end.downcase
73
+ end
74
+
75
+ # @!attribute [r] key
76
+ # @return [Binary] Private key used to encrypt and decrypt payloads
77
+ def key
78
+ attributes[:key]
79
+ end
80
+
81
+ # @!attribute [r] key_length
82
+ # @return [Integer] The length in bits of the +key+
83
+ def key_length
84
+ key.unpack('b*').first.length
85
+ end
86
+
87
+ # @!attribute [r] mode
88
+ # @return [String] The cipher mode, currently only +CBC+ is supported
89
+ def mode
90
+ attributes.fetch(:mode) do
91
+ Ably::Util::Crypto::DEFAULTS.fetch(:mode)
92
+ end.downcase
93
+ end
94
+
95
+ # @!attribute [r] cipher_type
96
+ # @return [String] The complete Cipher algorithm string such as AES-128-CBC
97
+ def cipher_type
98
+ self.class.cipher_type(algorithm: algorithm, key_length: key_length, mode: mode)
99
+ end
100
+
101
+ # @!attribute [r] attributes
102
+ # @return [Hash] Access the token details Hash object ruby'fied to use symbolized keys
103
+ def attributes
104
+ @attributes
105
+ end
106
+
107
+ private
108
+
109
+ def decode_key(encoded_key)
110
+ normalised_key = encoded_key.gsub('_', '/').gsub('-', '+')
111
+ Base64.decode64(normalised_key)
112
+ end
113
+ end
114
+ end
@@ -32,23 +32,25 @@ module Ably::Models
32
32
  #
33
33
  def initialize(attributes = {})
34
34
  @hash_object = IdiomaticRubyWrapper(attributes.clone)
35
- hash[:connection_state_ttl] = (hash[:connection_state_ttl].to_f / 1000).round if hash[:connection_state_ttl]
36
- hash.freeze
35
+ if self.attributes[:connection_state_ttl]
36
+ self.attributes[:connection_state_ttl] = (self.attributes[:connection_state_ttl].to_f / 1000).round
37
+ end
38
+ self.attributes.freeze
37
39
  end
38
40
 
39
41
  %w(client_id connection_key max_message_size max_frame_size max_inbound_rate connection_state_ttl server_id).each do |attribute|
40
42
  define_method attribute do
41
- hash[attribute.to_sym]
43
+ attributes[attribute.to_sym]
42
44
  end
43
45
  end
44
46
 
45
47
  def has_client_id?
46
- hash.has_key?(:client_id)
48
+ attributes.has_key?(:client_id)
47
49
  end
48
50
 
49
- # @!attribute [r] hash
51
+ # @!attribute [r] attributes
50
52
  # @return [Hash] Access the token details Hash object ruby'fied to use symbolized keys
51
- def hash
53
+ def attributes
52
54
  @hash_object
53
55
  end
54
56
  end
@@ -8,7 +8,7 @@ module Ably::Models
8
8
  # @return [Integer] Ably error code (see ably-common/protocol/errors.json)
9
9
  # @!attribute [r] status
10
10
  # @return [Integer] HTTP Status Code corresponding to this error, where applicable
11
- # @!attribute [r] hash
11
+ # @!attribute [r] attributes
12
12
  # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
13
13
  #
14
14
  class ErrorInfo
@@ -21,12 +21,12 @@ module Ably::Models
21
21
 
22
22
  %w(message code status_code).each do |attribute|
23
23
  define_method attribute do
24
- hash[attribute.to_sym]
24
+ attributes[attribute.to_sym]
25
25
  end
26
26
  end
27
27
  alias_method :status, :status_code
28
28
 
29
- def hash
29
+ def attributes
30
30
  @hash_object
31
31
  end
32
32
 
@@ -57,14 +57,14 @@ module Ably::Models
57
57
  $stderr.puts "<IdiomaticRubyWrapper#initialize> WARNING: Wrapping a IdiomaticRubyWrapper with another IdiomaticRubyWrapper"
58
58
  end
59
59
 
60
- @hash = mixedCaseHashObject
61
- @stop_at = Array(stop_at).each_with_object({}) do |key, hash|
62
- hash[convert_to_snake_case_symbol(key)] = true
60
+ @attributes = mixedCaseHashObject
61
+ @stop_at = Array(stop_at).each_with_object({}) do |key, object|
62
+ object[convert_to_snake_case_symbol(key)] = true
63
63
  end.freeze
64
64
  end
65
65
 
66
66
  def [](key)
67
- value = hash[source_key_for(key)]
67
+ value = attributes[source_key_for(key)]
68
68
  if stop_at?(key) || !value.kind_of?(Hash)
69
69
  value
70
70
  else
@@ -73,7 +73,7 @@ module Ably::Models
73
73
  end
74
74
 
75
75
  def []=(key, value)
76
- hash[source_key_for(key)] = value
76
+ attributes[source_key_for(key)] = value
77
77
  end
78
78
 
79
79
  def fetch(key, default = nil)
@@ -91,7 +91,7 @@ module Ably::Models
91
91
  end
92
92
 
93
93
  def size
94
- hash.size
94
+ attributes.size
95
95
  end
96
96
 
97
97
  def keys
@@ -103,14 +103,14 @@ module Ably::Models
103
103
  end
104
104
 
105
105
  def has_key?(key)
106
- hash.has_key?(source_key_for(key))
106
+ attributes.has_key?(source_key_for(key))
107
107
  end
108
108
 
109
109
  # Method ensuring this {IdiomaticRubyWrapper} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
110
110
  def each
111
111
  return to_enum(:each) unless block_given?
112
112
 
113
- hash.each do |key, value|
113
+ attributes.each do |key, value|
114
114
  key = convert_to_snake_case_symbol(key)
115
115
  value = self[key]
116
116
  yield key, value
@@ -138,9 +138,10 @@ module Ably::Models
138
138
  end
139
139
  end
140
140
 
141
- # Access to the raw Hash object provided to the constructer of this wrapper
142
- def hash
143
- @hash
141
+ # @!attribute [r] Hash
142
+ # @return [Hash] Access to the raw Hash object provided to the constructer of this wrapper
143
+ def attributes
144
+ @attributes
144
145
  end
145
146
 
146
147
  # Takes the underlying Hash object and returns it in as a JSON ready Hash object using snakeCase for compability with the Ably service.
@@ -149,7 +150,7 @@ module Ably::Models
149
150
  # wrapper = IdiomaticRubyWrapper({ 'mixedCase': true, mixed_case: false, 'snake_case': 1 })
150
151
  # wrapper.as_json({ 'mixedCase': true, 'snakeCase': 1 })
151
152
  def as_json(*args)
152
- hash.each_with_object({}) do |key_val, new_hash|
153
+ attributes.each_with_object({}) do |key_val, new_hash|
153
154
  key = key_val[0]
154
155
  mixed_case_key = convert_to_mixed_case(key)
155
156
  wrapped_val = self[key]
@@ -171,26 +172,32 @@ module Ably::Models
171
172
  # wrapper = IdiomaticRubyWrapper({ 'mixedCase': true, mixed_case: false, 'snake_case': 1 })
172
173
  # wrapper.to_hash({ mixed_case: true, snake_case: 1 })
173
174
  def to_hash(*args)
174
- each_with_object({}) do |key_val, hash|
175
- key, val = key_val
176
- val = val.to_hash(args) if val.kind_of?(IdiomaticRubyWrapper)
177
- hash[key] = val
175
+ each_with_object({}) do |key_val, object|
176
+ key, val = key_val
177
+ val = val.to_hash(args) if val.kind_of?(IdiomaticRubyWrapper)
178
+ object[key] = val
178
179
  end
179
180
  end
180
181
 
181
182
  # Method to create a duplicate of the underlying Hash object
182
183
  # Useful when underlying Hash is frozen
183
184
  def dup
184
- Ably::Models::IdiomaticRubyWrapper.new(hash.dup)
185
+ Ably::Models::IdiomaticRubyWrapper.new(attributes.dup, stop_at: stop_at.keys)
185
186
  end
186
187
 
187
188
  # Freeze the underlying data
188
189
  def freeze
189
- hash.freeze
190
+ attributes.freeze
190
191
  end
191
192
 
192
193
  def to_s
193
- hash.to_s
194
+ attributes.to_s
195
+ end
196
+
197
+ # @!attribute [r] hash
198
+ # @return [Integer] Compute a hash-code for this hash. Two hashes with the same content will have the same hash code
199
+ def hash
200
+ attributes.hash
194
201
  end
195
202
 
196
203
  private
@@ -214,7 +221,7 @@ module Ably::Models
214
221
  ]
215
222
 
216
223
  preferred_format = format_preferences.detect do |format|
217
- hash.has_key?(format.call(symbolized_key))
224
+ attributes.has_key?(format.call(symbolized_key))
218
225
  end || format_preferences.first
219
226
 
220
227
  preferred_format.call(symbolized_key)
@@ -34,7 +34,7 @@ module Ably::Models
34
34
  # @return [String] A globally unique message ID
35
35
  # @!attribute [r] connection_id
36
36
  # @return [String] The connection_id of the publisher of the message
37
- # @!attribute [r] hash
37
+ # @!attribute [r] attributes
38
38
  # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
39
39
  #
40
40
  class Message
@@ -45,17 +45,17 @@ module Ably::Models
45
45
 
46
46
  # {Message} initializer
47
47
  #
48
- # @param hash_object [Hash] object with the underlying message details
48
+ # @param attributes [Hash] object with the underlying message detail key value attributes
49
49
  # @param [Hash] options an options Hash for this initializer
50
50
  # @option options [ProtocolMessage] :protocol_message An optional protocol message to assocate the presence message with
51
51
  # @option options [Logger] :logger An optional Logger to be used by {Ably::Modules::SafeDeferrable} if an exception is caught in a callback
52
52
  #
53
- def initialize(hash_object, options = {})
53
+ def initialize(attributes, options = {})
54
54
  @logger = options[:logger] # Logger expected for SafeDeferrable
55
55
  @protocol_message = options[:protocol_message]
56
- @raw_hash_object = hash_object
56
+ @raw_hash_object = attributes
57
57
 
58
- set_hash_object hash_object
58
+ set_attributes_object attributes
59
59
 
60
60
  ensure_utf_8 :name, name, allow_nil: true
61
61
  ensure_utf_8 :client_id, client_id, allow_nil: true
@@ -64,32 +64,32 @@ module Ably::Models
64
64
 
65
65
  %w( name client_id encoding ).each do |attribute|
66
66
  define_method attribute do
67
- hash[attribute.to_sym]
67
+ attributes[attribute.to_sym]
68
68
  end
69
69
  end
70
70
 
71
71
  def data
72
- @data ||= hash[:data].freeze
72
+ @data ||= attributes[:data].freeze
73
73
  end
74
74
 
75
75
  def id
76
- hash.fetch(:id) { "#{protocol_message.id!}:#{protocol_message_index}" }
76
+ attributes.fetch(:id) { "#{protocol_message.id!}:#{protocol_message_index}" }
77
77
  end
78
78
 
79
79
  def connection_id
80
- hash.fetch(:connection_id) { protocol_message.connection_id if assigned_to_protocol_message? }
80
+ attributes.fetch(:connection_id) { protocol_message.connection_id if assigned_to_protocol_message? }
81
81
  end
82
82
 
83
83
  def timestamp
84
- if hash[:timestamp]
85
- as_time_from_epoch(hash[:timestamp])
84
+ if attributes[:timestamp]
85
+ as_time_from_epoch(attributes[:timestamp])
86
86
  else
87
87
  protocol_message.timestamp
88
88
  end
89
89
  end
90
90
 
91
- def hash
92
- @hash_object
91
+ def attributes
92
+ @attributes
93
93
  end
94
94
 
95
95
  def to_json(*args)
@@ -128,8 +128,8 @@ module Ably::Models
128
128
  protocol_message.messages.map(&:object_id).index(self.object_id)
129
129
  end
130
130
 
131
- def set_hash_object(hash)
132
- @hash_object = IdiomaticRubyWrapper(hash.clone.freeze, stop_at: [:data])
131
+ def set_attributes_object(new_attributes)
132
+ @attributes = IdiomaticRubyWrapper(new_attributes.clone.freeze, stop_at: [:data])
133
133
  end
134
134
 
135
135
  def logger
@@ -4,7 +4,7 @@ require 'ably/util/crypto'
4
4
 
5
5
  module Ably::Models::MessageEncoders
6
6
  # Cipher Encoder & Decoder that automatically encrypts & decrypts messages using Ably::Util::Crypto
7
- # when a channel has option encrypted: true.
7
+ # when a channel has the +:cipher+ channel option configured
8
8
  #
9
9
  class Cipher < Base
10
10
  ENCODING_ID = 'cipher'
@@ -20,16 +20,17 @@ module Ably::Models::MessageEncoders
20
20
 
21
21
  if channel_configured_for_encryption?(channel_options)
22
22
  add_encoding_to_message 'utf-8', message unless is_binary?(message) || is_utf8_encoded?(message)
23
-
24
23
  crypto = crypto_for(channel_options)
25
24
  message[:data] = crypto.encrypt(message[:data])
26
- add_encoding_to_message "#{ENCODING_ID}+#{crypto.cipher_type.downcase}", message
25
+ add_encoding_to_message "#{ENCODING_ID}+#{crypto.cipher_params.cipher_type.downcase}", message
27
26
  end
28
27
  rescue ArgumentError => e
29
28
  raise Ably::Exceptions::CipherError.new(e.message, nil, 92005)
30
29
  rescue RuntimeError => e
31
30
  if e.message.match(/unsupported cipher algorithm/i)
32
31
  raise Ably::Exceptions::CipherError.new(e.message, nil, 92004)
32
+ else
33
+ raise e
33
34
  end
34
35
  end
35
36
 
@@ -40,8 +41,8 @@ module Ably::Models::MessageEncoders
40
41
  end
41
42
 
42
43
  crypto = crypto_for(channel_options)
43
- unless crypto.cipher_type == cipher_algorithm(message).upcase
44
- raise Ably::Exceptions::CipherError.new("Cipher algorithm #{crypto.cipher_type} does not match message cipher algorithm of #{cipher_algorithm(message).upcase}", nil, 92002)
44
+ unless crypto.cipher_params.cipher_type == cipher_algorithm(message).upcase
45
+ raise Ably::Exceptions::CipherError.new("Cipher algorithm #{crypto.cipher_params.cipher_type} does not match message cipher algorithm of #{cipher_algorithm(message).upcase}", nil, 92002)
45
46
  end
46
47
 
47
48
  message[:data] = crypto.decrypt(message[:data])
@@ -61,11 +62,11 @@ module Ably::Models::MessageEncoders
61
62
  end
62
63
 
63
64
  def crypto_for(channel_options)
64
- @cryptos[channel_options.fetch(:cipher_params, :default)] ||= Ably::Util::Crypto.new(channel_options.fetch(:cipher_params, {}))
65
+ @cryptos[channel_options.to_s] ||= Ably::Util::Crypto.new(channel_options.fetch(:cipher, {}))
65
66
  end
66
67
 
67
68
  def channel_configured_for_encryption?(channel_options)
68
- channel_options.fetch(:encrypted, false)
69
+ channel_options[:cipher]
69
70
  end
70
71
 
71
72
  def is_cipher_encoded?(message)