ably 0.8.8 → 0.8.9

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