ably 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +9 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +8 -1
  6. data/Rakefile +10 -0
  7. data/ably.gemspec +18 -18
  8. data/lib/ably.rb +6 -5
  9. data/lib/ably/auth.rb +11 -14
  10. data/lib/ably/exceptions.rb +18 -15
  11. data/lib/ably/logger.rb +102 -0
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/message.rb +19 -5
  14. data/lib/ably/models/message_encoders/base.rb +107 -0
  15. data/lib/ably/models/message_encoders/base64.rb +39 -0
  16. data/lib/ably/models/message_encoders/cipher.rb +80 -0
  17. data/lib/ably/models/message_encoders/json.rb +33 -0
  18. data/lib/ably/models/message_encoders/utf8.rb +33 -0
  19. data/lib/ably/models/paginated_resource.rb +23 -6
  20. data/lib/ably/models/presence_message.rb +19 -7
  21. data/lib/ably/models/protocol_message.rb +5 -4
  22. data/lib/ably/models/token.rb +2 -2
  23. data/lib/ably/modules/channels_collection.rb +0 -3
  24. data/lib/ably/modules/conversions.rb +3 -3
  25. data/lib/ably/modules/encodeable.rb +68 -0
  26. data/lib/ably/modules/event_emitter.rb +10 -4
  27. data/lib/ably/modules/event_machine_helpers.rb +6 -4
  28. data/lib/ably/modules/http_helpers.rb +7 -2
  29. data/lib/ably/modules/model_common.rb +2 -0
  30. data/lib/ably/modules/state_emitter.rb +10 -1
  31. data/lib/ably/realtime.rb +19 -12
  32. data/lib/ably/realtime/channel.rb +26 -13
  33. data/lib/ably/realtime/client.rb +31 -7
  34. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -3
  35. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +13 -4
  36. data/lib/ably/realtime/connection.rb +152 -46
  37. data/lib/ably/realtime/connection/connection_manager.rb +168 -0
  38. data/lib/ably/realtime/connection/connection_state_machine.rb +56 -33
  39. data/lib/ably/realtime/connection/websocket_transport.rb +56 -29
  40. data/lib/ably/{models → realtime/models}/nil_channel.rb +1 -1
  41. data/lib/ably/realtime/presence.rb +38 -13
  42. data/lib/ably/rest.rb +7 -5
  43. data/lib/ably/rest/channel.rb +24 -3
  44. data/lib/ably/rest/client.rb +56 -17
  45. data/lib/ably/rest/middleware/encoder.rb +49 -0
  46. data/lib/ably/rest/middleware/exceptions.rb +3 -2
  47. data/lib/ably/rest/middleware/logger.rb +37 -0
  48. data/lib/ably/rest/presence.rb +10 -2
  49. data/lib/ably/util/crypto.rb +57 -29
  50. data/lib/ably/util/pub_sub.rb +11 -0
  51. data/lib/ably/version.rb +1 -1
  52. data/spec/acceptance/realtime/channel_spec.rb +65 -7
  53. data/spec/acceptance/realtime/connection_spec.rb +123 -27
  54. data/spec/acceptance/realtime/message_spec.rb +319 -34
  55. data/spec/acceptance/realtime/presence_history_spec.rb +58 -0
  56. data/spec/acceptance/realtime/presence_spec.rb +160 -18
  57. data/spec/acceptance/rest/auth_spec.rb +93 -49
  58. data/spec/acceptance/rest/base_spec.rb +10 -10
  59. data/spec/acceptance/rest/channel_spec.rb +35 -19
  60. data/spec/acceptance/rest/channels_spec.rb +8 -8
  61. data/spec/acceptance/rest/message_spec.rb +224 -0
  62. data/spec/acceptance/rest/presence_spec.rb +159 -23
  63. data/spec/acceptance/rest/stats_spec.rb +5 -5
  64. data/spec/acceptance/rest/time_spec.rb +4 -4
  65. data/spec/integration/rest/auth.rb +1 -1
  66. data/spec/resources/crypto-data-128.json +56 -0
  67. data/spec/resources/crypto-data-256.json +56 -0
  68. data/spec/rspec_config.rb +39 -0
  69. data/spec/spec_helper.rb +4 -42
  70. data/spec/support/api_helper.rb +1 -1
  71. data/spec/support/event_machine_helper.rb +0 -5
  72. data/spec/support/protocol_msgbus_helper.rb +3 -3
  73. data/spec/support/test_app.rb +3 -3
  74. data/spec/unit/logger_spec.rb +135 -0
  75. data/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  76. data/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  77. data/spec/unit/models/message_encoders/json_spec.rb +135 -0
  78. data/spec/unit/models/message_encoders/utf8_spec.rb +100 -0
  79. data/spec/unit/models/message_spec.rb +16 -1
  80. data/spec/unit/models/paginated_resource_spec.rb +46 -0
  81. data/spec/unit/models/presence_message_spec.rb +18 -5
  82. data/spec/unit/models/token_spec.rb +1 -1
  83. data/spec/unit/modules/event_emitter_spec.rb +24 -10
  84. data/spec/unit/realtime/channel_spec.rb +3 -3
  85. data/spec/unit/realtime/channels_spec.rb +1 -1
  86. data/spec/unit/realtime/client_spec.rb +44 -2
  87. data/spec/unit/realtime/connection_spec.rb +2 -2
  88. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +4 -4
  89. data/spec/unit/realtime/presence_spec.rb +1 -1
  90. data/spec/unit/realtime/realtime_spec.rb +3 -3
  91. data/spec/unit/realtime/websocket_transport_spec.rb +24 -0
  92. data/spec/unit/rest/channels_spec.rb +1 -1
  93. data/spec/unit/rest/client_spec.rb +45 -10
  94. data/spec/unit/util/crypto_spec.rb +82 -0
  95. data/spec/unit/{modules → util}/pub_sub_spec.rb +13 -1
  96. metadata +43 -12
  97. data/spec/acceptance/crypto.rb +0 -63
@@ -19,6 +19,17 @@ module Ably::Util
19
19
  class PubSub
20
20
  include Ably::Modules::EventEmitter
21
21
 
22
+ # Ensure new PubSub object does not share class instance variables
23
+ def self.new(options = {})
24
+ Class.new(PubSub).allocate.tap do |pub_sub_object|
25
+ pub_sub_object.send(:initialize, options)
26
+ end
27
+ end
28
+
29
+ def inspect
30
+ "<#PubSub: @event_emitter_coerce_proc: #{self.class.event_emitter_coerce_proc.inspect}\n @callbacks: #{callbacks}>"
31
+ end
32
+
22
33
  def initialize(options = {})
23
34
  self.class.instance_eval do
24
35
  configure_event_emitter options
data/lib/ably/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = "0.1.6"
2
+ VERSION = '0.2.0'
3
3
  end
@@ -8,13 +8,11 @@ describe Ably::Realtime::Channel do
8
8
  context "over #{protocol}" do
9
9
  let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
10
10
 
11
- let(:client) do
12
- Ably::Realtime::Client.new(default_options)
13
- end
11
+ let(:client) { Ably::Realtime::Client.new(default_options) }
14
12
  let(:channel_name) { SecureRandom.hex(2) }
15
- let(:payload) { SecureRandom.hex(4) }
16
- let(:channel) { client.channel(channel_name) }
17
- let(:messages) { [] }
13
+ let(:payload) { SecureRandom.hex(4) }
14
+ let(:channel) { client.channel(channel_name) }
15
+ let(:messages) { [] }
18
16
 
19
17
  it 'attaches to a channel' do
20
18
  run_reactor do
@@ -135,6 +133,66 @@ describe Ably::Realtime::Channel do
135
133
  end
136
134
  end
137
135
 
136
+ it 'opens many connections and then many channels simultaneously' do
137
+ run_reactor(15) do
138
+ count, connected_ids = 25, []
139
+
140
+ clients = count.times.map do
141
+ Ably::Realtime::Client.new(default_options)
142
+ end
143
+
144
+ channels_opened = 0
145
+ open_channels_on_clients = Proc.new do
146
+ 5.times.each do |channel|
147
+ clients.each do |client|
148
+ client.channel("channel-#{channel}").attach do
149
+ channels_opened += 1
150
+ if channels_opened == clients.count * 5
151
+ expect(channels_opened).to eql(clients.count * 5)
152
+ stop_reactor
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ clients.each do |client|
160
+ client.connection.on(:connected) do
161
+ connected_ids << client.connection.id
162
+
163
+ if connected_ids.count == 25
164
+ expect(connected_ids.uniq.count).to eql(25)
165
+ open_channels_on_clients.call
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ it 'opens many connections and attaches to channels before connected' do
173
+ run_reactor(15) do
174
+ count, connected_ids = 25, []
175
+
176
+ clients = count.times.map do
177
+ Ably::Realtime::Client.new(default_options)
178
+ end
179
+
180
+ channels_opened = 0
181
+
182
+ clients.each do |client|
183
+ 5.times.each do |channel|
184
+ client.channel("channel-#{channel}").attach do
185
+ channels_opened += 1
186
+ if channels_opened == clients.count * 5
187
+ expect(channels_opened).to eql(clients.count * 5)
188
+ stop_reactor
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+
138
196
  context 'attach failure' do
139
197
  let(:restricted_client) do
140
198
  Ably::Realtime::Client.new(default_options.merge(api_key: restricted_api_key))
@@ -146,7 +204,7 @@ describe Ably::Realtime::Channel do
146
204
  restricted_channel.attach
147
205
  restricted_channel.on(:failed) do |error|
148
206
  expect(restricted_channel.state).to eq(:failed)
149
- expect(error.status_code).to eq(401)
207
+ expect(error.status).to eq(401)
150
208
  stop_reactor
151
209
  end
152
210
  end
@@ -3,19 +3,43 @@ require 'spec_helper'
3
3
  describe Ably::Realtime::Connection do
4
4
  include RSpec::EventMachine
5
5
 
6
- [:msgpack, :json].each do |protocol|
6
+ let(:connection) { client.connection }
7
+
8
+ [:json, :msgpack].each do |protocol|
7
9
  context "over #{protocol}" do
10
+ let(:default_options) do
11
+ { api_key: api_key, environment: environment, protocol: protocol }
12
+ end
13
+
8
14
  let(:client) do
9
- Ably::Realtime::Client.new(api_key: api_key, environment: environment, protocol: protocol)
15
+ Ably::Realtime::Client.new(default_options)
10
16
  end
11
17
 
12
- subject { client.connection }
18
+ context 'with API key' do
19
+ it 'connects automatically' do
20
+ run_reactor do
21
+ connection.on(:connected) do
22
+ expect(connection.state).to eq(:connected)
23
+ expect(client.auth.auth_params[:key_id]).to_not be_nil
24
+ expect(client.auth.auth_params[:access_token]).to be_nil
25
+ stop_reactor
26
+ end
27
+ end
28
+ end
29
+ end
13
30
 
14
- it 'connects automatically' do
15
- run_reactor do
16
- subject.on(:connected) do
17
- expect(subject.state).to eq(:connected)
18
- stop_reactor
31
+ context 'with client_id resulting in token auth' do
32
+ let(:default_options) do
33
+ { api_key: api_key, environment: environment, protocol: protocol, client_id: SecureRandom.hex, log_level: :debug }
34
+ end
35
+ it 'connects automatically' do
36
+ run_reactor do
37
+ connection.on(:connected) do
38
+ expect(connection.state).to eq(:connected)
39
+ expect(client.auth.auth_params[:access_token]).to_not be_nil
40
+ expect(client.auth.auth_params[:key_id]).to be_nil
41
+ stop_reactor
42
+ end
19
43
  end
20
44
  end
21
45
  end
@@ -32,7 +56,7 @@ describe Ably::Realtime::Connection do
32
56
 
33
57
  run_reactor do
34
58
  phases.each do |phase|
35
- subject.on(phase) do
59
+ connection.on(phase) do
36
60
  events_triggered << phase
37
61
  test_expectation.call if events_triggered.length == phases.length
38
62
  end
@@ -41,24 +65,38 @@ describe Ably::Realtime::Connection do
41
65
  end
42
66
  end
43
67
 
44
- skip '#closed disconnects and closes the connection once timeout is reached'
68
+ skip '#close disconnects, closes the connection immediately and changes the connection state to closed'
45
69
 
46
- specify '#closed disconnects and closes the connection gracefully' do
70
+ specify '#close(graceful: true) gracefully waits for the server to close the connection' do
47
71
  run_reactor(8) do
48
- subject.close
49
- subject.on(:closed) do
50
- expect(subject.state).to eq(:closed)
72
+ connection.close
73
+ connection.on(:closed) do
74
+ expect(connection.state).to eq(:closed)
51
75
  stop_reactor
52
76
  end
53
77
  end
54
78
  end
55
79
 
56
- it 'receives a heart beat' do
57
- run_reactor(20) do
58
- subject.on(:connected) do
59
- subject.__incoming_protocol_msgbus__.subscribe(:message) do |protocol_message|
60
- if protocol_message.action == :heartbeat
61
- expect(protocol_message.action).to eq(:heartbeat)
80
+ it 'echoes a heart beat with #ping' do
81
+ run_reactor do
82
+ connection.on(:connected) do
83
+ connection.ping do |time_elapsed|
84
+ expect(time_elapsed).to be > 0
85
+ stop_reactor
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ skip 'connects, closes gracefully and reconnects on #connect'
92
+
93
+ it 'connects, closes the connection, and then reconnects with a new connection ID' do
94
+ run_reactor(15) do
95
+ connection.connect do
96
+ connection_id = connection.id
97
+ connection.close do
98
+ connection.connect do
99
+ expect(connection.id).to_not eql(connection_id)
62
100
  stop_reactor
63
101
  end
64
102
  end
@@ -66,15 +104,73 @@ describe Ably::Realtime::Connection do
66
104
  end
67
105
  end
68
106
 
69
- skip 'connects, closes gracefully and reconnects on #connect'
107
+ context 'failures' do
108
+ context 'with invalid app part of the key' do
109
+ let(:missing_key) { 'not_an_app.invalid_key_id:invalid_key_value' }
110
+ let(:client) do
111
+ Ably::Realtime::Client.new(default_options.merge(api_key: missing_key))
112
+ end
70
113
 
71
- it 'connects, closes then connection when timeout is reaached and reconnects on #connect' do
114
+ it 'enters the failed state and returns a not found error' do
115
+ run_reactor do
116
+ connection.on(:failed) do |error|
117
+ expect(connection.state).to eq(:failed)
118
+ expect(error.status).to eq(404)
119
+ stop_reactor
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ context 'with invalid key ID part of the key' do
126
+ let(:invalid_key) { "#{app_id}.invalid_key_id:invalid_key_value" }
127
+ let(:client) do
128
+ Ably::Realtime::Client.new(default_options.merge(api_key: invalid_key))
129
+ end
130
+
131
+ it 'enters the failed state and returns an authorization error' do
132
+ run_reactor do
133
+ connection.on(:failed) do |error|
134
+ expect(connection.state).to eq(:failed)
135
+ expect(error.status).to eq(401)
136
+ stop_reactor
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ context 'with invalid WebSocket host' do
143
+ let(:client) do
144
+ Ably::Realtime::Client.new(default_options.merge(ws_host: 'non.existent.host'))
145
+ end
146
+
147
+ it 'enters the failed state and returns an authorization error' do
148
+ run_reactor do
149
+ connection.on(:failed) do |error|
150
+ expect(connection.state).to eq(:failed)
151
+ expect(error.code).to eq(80000)
152
+ expect(error.status).to be_nil
153
+ stop_reactor
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ it 'opens many connections simultaneously' do
72
161
  run_reactor(15) do
73
- subject.connect do
74
- connection_id = subject.id
75
- subject.close do
76
- subject.connect do
77
- expect(subject.id).to_not eql(connection_id)
162
+ count, connected_ids = 25, []
163
+
164
+ clients = count.times.map do
165
+ Ably::Realtime::Client.new(default_options)
166
+ end
167
+
168
+ clients.each do |client|
169
+ client.connection.on(:connected) do
170
+ connected_ids << client.connection.id
171
+
172
+ if connected_ids.count == 25
173
+ expect(connected_ids.uniq.count).to eql(25)
78
174
  stop_reactor
79
175
  end
80
176
  end
@@ -1,5 +1,7 @@
1
1
  require 'spec_helper'
2
2
  require 'securerandom'
3
+ require 'json'
4
+ require 'base64'
3
5
 
4
6
  describe 'Ably::Realtime::Channel Messages' do
5
7
  include RSpec::EventMachine
@@ -18,8 +20,8 @@ describe 'Ably::Realtime::Channel Messages' do
18
20
  let(:other_client_channel) { other_client.channel(channel_name) }
19
21
 
20
22
  let(:channel_name) { 'subscribe_send_text' }
21
- let(:options) { { :protocol => :json } }
22
- let(:payload) { 'Test message (subscribe_send_text)' }
23
+ let(:options) { { :protocol => :json } }
24
+ let(:payload) { 'Test message (subscribe_send_text)' }
23
25
 
24
26
  it 'sends a string message' do
25
27
  run_reactor do
@@ -52,7 +54,7 @@ describe 'Ably::Realtime::Channel Messages' do
52
54
  let(:no_echo_channel) { no_echo_client.channel(channel_name) }
53
55
 
54
56
  it 'sends a single message without a reply yet the messages is echoed on another normal connection' do
55
- run_reactor do
57
+ run_reactor(10) do
56
58
  channel.attach do |echo_channel|
57
59
  no_echo_channel.attach do
58
60
  no_echo_channel.publish 'test_event', payload
@@ -63,7 +65,9 @@ describe 'Ably::Realtime::Channel Messages' do
63
65
 
64
66
  echo_channel.subscribe('test_event') do |message|
65
67
  expect(message.data).to eql(payload)
66
- EventMachine.add_timer(1.5) { stop_reactor }
68
+ EventMachine.add_timer(1) do
69
+ stop_reactor
70
+ end
67
71
  end
68
72
  end
69
73
  end
@@ -72,9 +76,9 @@ describe 'Ably::Realtime::Channel Messages' do
72
76
  end
73
77
 
74
78
  context 'with multiple messages' do
75
- let(:send_count) { 15 }
79
+ let(:send_count) { 15 }
76
80
  let(:expected_echos) { send_count * 2 }
77
- let(:channel_name) { SecureRandom.hex }
81
+ let(:channel_name) { SecureRandom.hex }
78
82
  let(:echos) do
79
83
  { client: 0, other: 0 }
80
84
  end
@@ -82,37 +86,28 @@ describe 'Ably::Realtime::Channel Messages' do
82
86
  { client: 0, other: 0 }
83
87
  end
84
88
 
85
- def expect_messages_to_be_echoed_on_both_connections
86
- {
87
- channel => :client,
88
- other_client_channel => :other
89
- }.each do |target_channel, echo_key|
90
- EventMachine.defer do
91
- target_channel.subscribe('test_event') do |message|
92
- echos[echo_key] += 1
93
-
94
- if echos[:client] == expected_echos && echos[:other] == expected_echos
95
- # Wait briefly before doing the final check in case additional messages received
96
- EventMachine.add_timer(0.5) do
97
- expect(echos[:client]).to eql(expected_echos)
98
- expect(echos[:other]).to eql(expected_echos)
99
- expect(callbacks[:client]).to eql(send_count)
100
- expect(callbacks[:other]).to eql(send_count)
101
- stop_reactor
102
- end
89
+ it 'sends and receives the messages on both opened connections and calls the callbacks (expects twice number of messages due to local echos)' do
90
+ run_reactor(8) do
91
+ check_message_and_callback_counts = Proc.new do
92
+ if echos[:client] == expected_echos && echos[:other] == expected_echos
93
+ # Wait for message backlog to clear
94
+ EventMachine.add_timer(0.5) do
95
+ expect(echos[:client]).to eql(expected_echos)
96
+ expect(echos[:other]).to eql(expected_echos)
97
+
98
+ expect(callbacks[:client]).to eql(send_count)
99
+ expect(callbacks[:other]).to eql(send_count)
100
+
101
+ EventMachine.stop
103
102
  end
104
103
  end
105
104
  end
106
- end
107
- end
108
105
 
109
- it 'sends and receives the messages on both opened connections (4 x send count due to local echos) and calls the callbacks' do
110
- run_reactor(10) do
111
- channel.attach
112
- other_client_channel.attach
106
+ published = false
107
+ attach_callback = Proc.new do
108
+ next if published
113
109
 
114
- channel.on(:attached) do
115
- other_client_channel.on(:attached) do
110
+ if channel.attached? && other_client_channel.attached?
116
111
  send_count.times do |index|
117
112
  channel.publish('test_event', "#{index}: #{payload}") do
118
113
  callbacks[:client] += 1
@@ -121,9 +116,22 @@ describe 'Ably::Realtime::Channel Messages' do
121
116
  callbacks[:other] += 1
122
117
  end
123
118
  end
124
- expect_messages_to_be_echoed_on_both_connections
119
+
120
+ published = true
125
121
  end
126
122
  end
123
+
124
+ channel.subscribe('test_event') do |message|
125
+ echos[:client] += 1
126
+ check_message_and_callback_counts.call
127
+ end
128
+ other_client_channel.subscribe('test_event') do |message|
129
+ echos[:other] += 1
130
+ check_message_and_callback_counts.call
131
+ end
132
+
133
+ channel.attach &attach_callback
134
+ other_client_channel.attach &attach_callback
127
135
  end
128
136
  end
129
137
  end
@@ -133,7 +141,7 @@ describe 'Ably::Realtime::Channel Messages' do
133
141
  Ably::Realtime::Client.new(options.merge(api_key: restricted_api_key, environment: environment, protocol: protocol))
134
142
  end
135
143
  let(:restricted_channel) { restricted_client.channel("cansubscribe:example") }
136
- let(:payload) { 'Test message without permission to publish' }
144
+ let(:payload) { 'Test message without permission to publish' }
137
145
 
138
146
  it 'calls the error callback' do
139
147
  run_reactor do
@@ -153,6 +161,283 @@ describe 'Ably::Realtime::Channel Messages' do
153
161
  end
154
162
  end
155
163
  end
164
+
165
+ context 'encoding and decoding encrypted messages' do
166
+ shared_examples 'an Ably encrypter and decrypter' do |item, data|
167
+ let(:algorithm) { data['algorithm'].upcase }
168
+ let(:mode) { data['mode'].upcase }
169
+ let(:key_length) { data['keylength'] }
170
+ let(:secret_key) { Base64.decode64(data['key']) }
171
+ let(:iv) { Base64.decode64(data['iv']) }
172
+
173
+ let(:cipher_options) { { key: secret_key, iv: iv, algorithm: algorithm, mode: mode, key_length: key_length } }
174
+
175
+ context 'publish & subscribe' do
176
+ let(:encoded) { item['encoded'] }
177
+ let(:encoded_data) { encoded['data'] }
178
+ let(:encoded_encoding) { encoded['encoding'] }
179
+ let(:encoded_data_decoded) do
180
+ if encoded_encoding == 'json'
181
+ JSON.parse(encoded_data)
182
+ elsif encoded_encoding == 'base64'
183
+ Base64.decode64(encoded_data)
184
+ else
185
+ encoded_data
186
+ end
187
+ end
188
+
189
+ let(:encrypted) { item['encrypted'] }
190
+ let(:encrypted_data) { encrypted['data'] }
191
+ let(:encrypted_encoding) { encrypted['encoding'] }
192
+ let(:encrypted_data_decoded) do
193
+ if encrypted_encoding.match(%r{/base64$})
194
+ Base64.decode64(encrypted_data)
195
+ else
196
+ encrypted_data
197
+ end
198
+ end
199
+
200
+ let(:encrypted_channel) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
201
+
202
+ it 'encrypts message automatically when published' do
203
+ run_reactor do
204
+ encrypted_channel.__incoming_msgbus__.unsubscribe # remove all subscribe callbacks that could decrypt the message
205
+
206
+ encrypted_channel.__incoming_msgbus__.subscribe(:message) do |message|
207
+ if protocol == :json
208
+ expect(message['encoding']).to eql(encrypted_encoding)
209
+ expect(message['data']).to eql(encrypted_data)
210
+ else
211
+ # Messages received over binary protocol will not have Base64 encoded data
212
+ expect(message['encoding']).to eql(encrypted_encoding.gsub(%r{/base64$}, ''))
213
+ expect(message['data']).to eql(encrypted_data_decoded)
214
+ end
215
+ stop_reactor
216
+ end
217
+
218
+ encrypted_channel.publish 'example', encoded_data_decoded
219
+ end
220
+ end
221
+
222
+ it 'sends and receives messages that are encrypted & decrypted by the Ably library' do
223
+ run_reactor do
224
+ encrypted_channel.publish 'example', encoded_data_decoded
225
+ encrypted_channel.subscribe do |message|
226
+ expect(message.data).to eql(encoded_data_decoded)
227
+ expect(message.encoding).to be_nil
228
+ stop_reactor
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ resources_root = File.expand_path('../../../resources', __FILE__)
236
+
237
+ def self.add_tests_for_data(data)
238
+ data['items'].each_with_index do |item, index|
239
+ context "item #{index} with encrypted encoding #{item['encrypted']['encoding']}" do
240
+ it_behaves_like 'an Ably encrypter and decrypter', item, data
241
+ end
242
+ end
243
+ end
244
+
245
+ context 'with AES-128-CBC' do
246
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-128.json')))
247
+ add_tests_for_data data
248
+ end
249
+
250
+ context 'with AES-256-CBC' do
251
+ data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-256.json')))
252
+ add_tests_for_data data
253
+ end
254
+
255
+ context 'multiple sends from one client to another' do
256
+ let(:cipher_options) { { key: SecureRandom.hex(32) } }
257
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
258
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
259
+
260
+ let(:data) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
261
+ let(:message_count) { 50 }
262
+
263
+ it 'encrypt and decrypt messages' do
264
+ messages_received = {
265
+ decrypted: 0,
266
+ encrypted: 0
267
+ }
268
+
269
+ run_reactor do
270
+ encrypted_channel_client2.attach do
271
+ encrypted_channel_client2.subscribe do |message|
272
+ expect(message.data).to eql("#{message.name}-#{data}")
273
+ expect(message.encoding).to be_nil
274
+ messages_received[:decrypted] += 1
275
+ stop_reactor if messages_received[:decrypted] == message_count
276
+ end
277
+
278
+ encrypted_channel_client1.__incoming_msgbus__.subscribe(:message) do |message|
279
+ expect(message['encoding']).to match(/cipher\+/)
280
+ messages_received[:encrypted] += 1
281
+ end
282
+ end
283
+
284
+ message_count.times do |index|
285
+ encrypted_channel_client2.publish index.to_s, "#{index}-#{data}"
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ context "sending using protocol #{protocol} and subscribing with a different protocol" do
292
+ let(:other_protocol) { protocol == :msgpack ? :json : :msgpack }
293
+ let(:other_client) do
294
+ Ably::Realtime::Client.new(default_options.merge(protocol: other_protocol))
295
+ end
296
+
297
+ let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
298
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
299
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
300
+
301
+ before do
302
+ expect(other_client.protocol_binary?).to_not eql(client.protocol_binary?)
303
+ end
304
+
305
+ [MessagePack.pack({ 'key' => SecureRandom.hex }), '€ unicode', { 'key' => SecureRandom.hex }].each do |payload|
306
+ payload_description = "#{payload.class}#{" #{payload.encoding}" if payload.kind_of?(String)}"
307
+
308
+ specify "delivers a #{payload_description} payload to the receiver" do
309
+ run_reactor do
310
+ encrypted_channel_client1.publish 'example', payload
311
+ encrypted_channel_client2.subscribe do |message|
312
+ expect(message.data).to eql(payload)
313
+ expect(message.encoding).to be_nil
314
+ stop_reactor
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ context 'publishing on an unencrypted channel and subscribing on an encrypted channel with another client' do
322
+ let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
323
+ let(:unencrypted_channel_client1) { client.channel(channel_name) }
324
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
325
+
326
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
327
+
328
+ it 'does not attempt to decrypt the message' do
329
+ run_reactor do
330
+ unencrypted_channel_client1.publish 'example', payload
331
+ encrypted_channel_client2.subscribe do |message|
332
+ expect(message.data).to eql(payload)
333
+ expect(message.encoding).to be_nil
334
+ stop_reactor
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ context 'publishing on an encrypted channel and subscribing on an unencrypted channel with another client' do
341
+ let(:cipher_options) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
342
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
343
+ let(:unencrypted_channel_client2) { other_client.channel(channel_name) }
344
+
345
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
346
+
347
+ it 'delivers the message but still encrypted' do
348
+ run_reactor do
349
+ encrypted_channel_client1.publish 'example', payload
350
+ unencrypted_channel_client2.subscribe do |message|
351
+ expect(message.data).to_not eql(payload)
352
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
353
+ stop_reactor
354
+ end
355
+ end
356
+ end
357
+
358
+ it 'triggers a Cipher error on the channel' do
359
+ run_reactor do
360
+ unencrypted_channel_client2.attach do
361
+ encrypted_channel_client1.publish 'example', payload
362
+ unencrypted_channel_client2.on(:error) do |error|
363
+ expect(error).to be_a(Ably::Exceptions::CipherError)
364
+ expect(error.code).to eql(92001)
365
+ expect(error.message).to match(/Message cannot be decrypted/)
366
+ stop_reactor
367
+ end
368
+ end
369
+ end
370
+ end
371
+ end
372
+
373
+ context 'publishing on an encrypted channel and subscribing with a different algorithm on another client' do
374
+ let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
375
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
376
+ let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 128 } }
377
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
378
+
379
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
380
+
381
+ it 'delivers the message but still encrypted' do
382
+ run_reactor do
383
+ encrypted_channel_client1.publish 'example', payload
384
+ encrypted_channel_client2.subscribe do |message|
385
+ expect(message.data).to_not eql(payload)
386
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
387
+ stop_reactor
388
+ end
389
+ end
390
+ end
391
+
392
+ it 'triggers a Cipher error on the channel' do
393
+ run_reactor do
394
+ encrypted_channel_client2.attach do
395
+ encrypted_channel_client1.publish 'example', payload
396
+ encrypted_channel_client2.on(:error) do |error|
397
+ expect(error).to be_a(Ably::Exceptions::CipherError)
398
+ expect(error.code).to eql(92002)
399
+ expect(error.message).to match(/Cipher algorithm [\w\d-]+ does not match/)
400
+ stop_reactor
401
+ end
402
+ end
403
+ end
404
+ end
405
+ end
406
+
407
+ context 'publishing on an encrypted channel and subscribing with a different key on another client' do
408
+ let(:cipher_options_client1) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
409
+ let(:encrypted_channel_client1) { client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client1) }
410
+ let(:cipher_options_client2) { { key: SecureRandom.hex(32), algorithm: 'aes', mode: 'cbc', key_length: 256 } }
411
+ let(:encrypted_channel_client2) { other_client.channel(channel_name, encrypted: true, cipher_params: cipher_options_client2) }
412
+
413
+ let(:payload) { MessagePack.pack({ 'key' => SecureRandom.hex }) }
414
+
415
+ it 'delivers the message but still encrypted' do
416
+ run_reactor do
417
+ encrypted_channel_client1.publish 'example', payload
418
+ encrypted_channel_client2.subscribe do |message|
419
+ expect(message.data).to_not eql(payload)
420
+ expect(message.encoding).to match(/^cipher\+aes-256-cbc/)
421
+ stop_reactor
422
+ end
423
+ end
424
+ end
425
+
426
+ it 'triggers a Cipher error on the channel' do
427
+ run_reactor do
428
+ encrypted_channel_client2.attach do
429
+ encrypted_channel_client1.publish 'example', payload
430
+ encrypted_channel_client2.on(:error) do |error|
431
+ expect(error).to be_a(Ably::Exceptions::CipherError)
432
+ expect(error.code).to eql(92003)
433
+ expect(error.message).to match(/CipherError decrypting data/)
434
+ stop_reactor
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end
156
441
  end
157
442
  end
158
443
  end