ably-rest 1.1.2 → 1.2.2

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/CHANGELOG.md +1 -1
  4. data/MAINTAINERS.md +1 -0
  5. data/README.md +4 -2
  6. data/ably-rest.gemspec +15 -18
  7. data/lib/ably-rest.rb +2 -0
  8. data/lib/submodules/ably-ruby/.github/workflows/check.yml +50 -0
  9. data/lib/submodules/ably-ruby/CHANGELOG.md +200 -0
  10. data/lib/submodules/ably-ruby/COPYRIGHT +1 -0
  11. data/lib/submodules/ably-ruby/LICENSE +172 -11
  12. data/lib/submodules/ably-ruby/MAINTAINERS.md +1 -0
  13. data/lib/submodules/ably-ruby/README.md +24 -22
  14. data/lib/submodules/ably-ruby/SPEC.md +1020 -929
  15. data/lib/submodules/ably-ruby/UPDATING.md +30 -0
  16. data/lib/submodules/ably-ruby/ably.gemspec +16 -23
  17. data/lib/submodules/ably-ruby/lib/ably/agent.rb +3 -0
  18. data/lib/submodules/ably-ruby/lib/ably/auth.rb +20 -10
  19. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +8 -2
  20. data/lib/submodules/ably-ruby/lib/ably/logger.rb +4 -4
  21. data/lib/submodules/ably-ruby/lib/ably/models/channel_details.rb +59 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/channel_metrics.rb +84 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/channel_occupancy.rb +43 -0
  24. data/lib/submodules/ably-ruby/lib/ably/models/channel_options.rb +97 -0
  25. data/lib/submodules/ably-ruby/lib/ably/models/channel_status.rb +53 -0
  26. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +8 -0
  27. data/lib/submodules/ably-ruby/lib/ably/models/delta_extras.rb +29 -0
  28. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +1 -1
  29. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +6 -2
  30. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -0
  31. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +28 -3
  32. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +14 -0
  33. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +31 -14
  34. data/lib/submodules/ably-ruby/lib/ably/models/token_details.rb +7 -2
  35. data/lib/submodules/ably-ruby/lib/ably/models/token_request.rb +1 -1
  36. data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +11 -1
  37. data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +22 -2
  38. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +34 -0
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +2 -2
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +19 -7
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_properties.rb +24 -0
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +10 -1
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +6 -0
  44. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +56 -28
  45. data/lib/submodules/ably-ruby/lib/ably/realtime/channels.rb +1 -1
  46. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -6
  47. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +9 -0
  48. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +13 -4
  49. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +4 -0
  50. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +67 -1
  51. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +6 -5
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +0 -14
  53. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +44 -29
  54. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +60 -29
  55. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/encoder.rb +1 -1
  56. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -1
  57. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/external_exceptions.rb +1 -1
  58. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +5 -2
  59. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +1 -1
  60. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_json.rb +1 -1
  61. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +1 -1
  62. data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +1 -1
  63. data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -14
  64. data/lib/submodules/ably-ruby/lib/ably.rb +1 -0
  65. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +4 -4
  66. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +25 -0
  67. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +476 -21
  68. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channels_spec.rb +59 -7
  69. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +72 -16
  70. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +85 -13
  71. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +301 -34
  72. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +77 -0
  73. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +3 -59
  74. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +84 -158
  75. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +3 -19
  76. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +24 -75
  77. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +8 -4
  78. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +141 -10
  79. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +23 -6
  80. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +146 -47
  81. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +61 -3
  82. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +3 -19
  83. data/lib/submodules/ably-ruby/spec/lib/unit/models/channel_options_spec.rb +52 -0
  84. data/lib/submodules/ably-ruby/spec/run_parallel_tests +2 -7
  85. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +131 -8
  86. data/lib/submodules/ably-ruby/spec/shared/model_behaviour.rb +1 -1
  87. data/lib/submodules/ably-ruby/spec/spec_helper.rb +12 -2
  88. data/lib/submodules/ably-ruby/spec/support/serialization_helper.rb +21 -0
  89. data/lib/submodules/ably-ruby/spec/support/test_app.rb +3 -3
  90. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +6 -14
  91. data/lib/submodules/ably-ruby/spec/unit/models/channel_details_spec.rb +30 -0
  92. data/lib/submodules/ably-ruby/spec/unit/models/channel_metrics_spec.rb +42 -0
  93. data/lib/submodules/ably-ruby/spec/unit/models/channel_occupancy_spec.rb +17 -0
  94. data/lib/submodules/ably-ruby/spec/unit/models/channel_status_spec.rb +36 -0
  95. data/lib/submodules/ably-ruby/spec/unit/models/delta_extras_spec.rb +14 -0
  96. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +17 -1
  97. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +97 -0
  98. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +49 -0
  99. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +125 -27
  100. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +14 -0
  101. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +3 -2
  102. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +53 -15
  103. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +19 -6
  104. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +38 -0
  105. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +44 -1
  106. data/lib/submodules/ably-ruby/spec/unit/rest/channels_spec.rb +81 -14
  107. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +47 -0
  108. data/spec/spec_helper.rb +5 -0
  109. data/spec/unit/client_spec.rb +30 -0
  110. metadata +88 -25
  111. data/lib/submodules/ably-ruby/.travis.yml +0 -19
@@ -121,15 +121,23 @@ module Ably::Realtime
121
121
  presence.manager.sync_process_messages protocol_message.channel_serial, protocol_message.presence
122
122
 
123
123
  when ACTION.Presence
124
- presence = get_channel(protocol_message.channel).presence
125
- protocol_message.presence.each do |presence_message|
126
- presence.__incoming_msgbus__.publish :presence, presence_message
124
+ if protocol_message.has_correct_message_size?
125
+ presence = get_channel(protocol_message.channel).presence
126
+ protocol_message.presence.each do |presence_message|
127
+ presence.__incoming_msgbus__.publish :presence, presence_message
128
+ end
129
+ else
130
+ logger.fatal Ably::Exceptions::ProtocolError.new("Not published. Channel message limit exceeded #{protocol_message.message_size} bytes", 400, Ably::Exceptions::Codes::UNABLE_TO_RECOVER_CHANNEL_MESSAGE_LIMIT_EXCEEDED).message
127
131
  end
128
132
 
129
133
  when ACTION.Message
130
- channel = get_channel(protocol_message.channel)
131
- protocol_message.messages.each do |message|
132
- channel.__incoming_msgbus__.publish :message, message
134
+ if protocol_message.has_correct_message_size?
135
+ channel = get_channel(protocol_message.channel)
136
+ protocol_message.messages.each do |message|
137
+ channel.__incoming_msgbus__.publish :message, message
138
+ end
139
+ else
140
+ logger.fatal Ably::Exceptions::ProtocolError.new("Not published. Channel message limit exceeded #{protocol_message.message_size} bytes", 400, Ably::Exceptions::Codes::UNABLE_TO_RECOVER_CHANNEL_MESSAGE_LIMIT_EXCEEDED).message
133
141
  end
134
142
 
135
143
  when ACTION.Auth
@@ -65,11 +65,16 @@ module Ably
65
65
  # @return [String,Nil]
66
66
  attr_reader :recover
67
67
 
68
+ # Additional parameters to be sent in the querystring when initiating a realtime connection
69
+ # @return [Hash]
70
+ attr_reader :transport_params
71
+
68
72
  def_delegators :auth, :client_id, :auth_options
69
73
  def_delegators :@rest_client, :encoders
70
74
  def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary?
71
75
  def_delegators :@rest_client, :environment, :custom_host, :custom_port, :custom_tls_port
72
76
  def_delegators :@rest_client, :log_level
77
+ def_delegators :@rest_client, :options
73
78
 
74
79
  # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection.
75
80
  #
@@ -82,6 +87,7 @@ module Ably
82
87
  # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection
83
88
  # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery
84
89
  # @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect.
90
+ # @option options [Hash] :transport_params Additional parameters to be sent in the querystring when initiating a realtime connection. Keys are Strings, values are Stringifiable(a value must respond to #to_s)
85
91
  #
86
92
  # @option options [Integer] :channel_retry_timeout (15 seconds). When a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
87
93
  # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in seconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
@@ -109,6 +115,9 @@ module Ably
109
115
  end
110
116
  end
111
117
 
118
+ @transport_params = options.delete(:transport_params).to_h.each_with_object({}) do |(key, value), acc|
119
+ acc[key.to_s] = value.to_s
120
+ end
112
121
  @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self))
113
122
  @echo_messages = rest_client.options.fetch(:echo_messages, true) == false ? false : true
114
123
  @queue_messages = rest_client.options.fetch(:queue_messages, true) == false ? false : true
@@ -117,17 +117,17 @@ module Ably::Realtime
117
117
  EventMachine.next_tick { connection.trigger_resumed }
118
118
  resend_pending_message_ack_queue
119
119
  else
120
- logger.debug { "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connection ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
120
+ logger.debug { "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connection ID #{protocol_message.connection_id} and key #{protocol_message.connection_details.connection_key}" }
121
121
  nack_messages_on_all_channels protocol_message.error
122
122
  force_reattach_on_channels protocol_message.error
123
123
  end
124
124
  else
125
- logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
125
+ logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_details.connection_key}" }
126
126
  end
127
127
 
128
128
  reattach_suspended_channels protocol_message.error
129
129
 
130
- connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
130
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key, protocol_message.connection_serial
131
131
  end
132
132
 
133
133
  # When connection is CONNECTED and receives an update
@@ -139,7 +139,7 @@ module Ably::Realtime
139
139
  # Update the connection details and any associated defaults
140
140
  connection.set_connection_details protocol_message.connection_details
141
141
 
142
- connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
142
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key, protocol_message.connection_serial
143
143
 
144
144
  state_change = Ably::Models::ConnectionStateChange.new(
145
145
  current: connection.state,
@@ -319,6 +319,15 @@ module Ably::Realtime
319
319
  end
320
320
  end
321
321
 
322
+ # @api private
323
+ def reintialize_failed_chanels
324
+ channels.select do |channel|
325
+ channel.failed?
326
+ end.each do |channel|
327
+ channel.transition_state_machine :initialized
328
+ end
329
+ end
330
+
322
331
  # When continuity on a connection is lost all messages
323
332
  # whether queued or awaiting an ACK must be NACK'd as we now have a new connection
324
333
  def nack_messages_on_all_channels(error)
@@ -36,6 +36,10 @@ module Ably::Realtime
36
36
  connection.manager.setup_transport
37
37
  end
38
38
 
39
+ after_transition(to: [:connecting], from: [:failed]) do |connection|
40
+ connection.manager.reintialize_failed_chanels
41
+ end
42
+
39
43
  after_transition(to: [:connecting], from: [:disconnected, :suspended]) do |connection|
40
44
  connection.manager.reconnect_transport
41
45
  end
@@ -1,3 +1,5 @@
1
+ require 'openssl'
2
+
1
3
  module Ably::Realtime
2
4
  class Connection
3
5
  # EventMachine WebSocket transport
@@ -16,10 +18,13 @@ module Ably::Realtime
16
18
  )
17
19
  include Ably::Modules::StateEmitter
18
20
 
21
+ attr_reader :host
22
+
19
23
  def initialize(connection, url)
20
24
  @connection = connection
21
25
  @state = STATE.Initialized
22
26
  @url = url
27
+ @host = URI.parse(url).hostname
23
28
 
24
29
  setup_event_handlers
25
30
  end
@@ -49,7 +54,7 @@ module Ably::Realtime
49
54
  # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
50
55
  def connection_completed
51
56
  change_state STATE.Connected
52
- start_tls if client.use_tls?
57
+ start_tls(tls_opts) if client.use_tls?
53
58
  driver.start
54
59
  end
55
60
 
@@ -77,6 +82,51 @@ module Ably::Realtime
77
82
  send_data(data)
78
83
  end
79
84
 
85
+ # TLS verification support, original implementation by Mislav Marohnić:
86
+ #
87
+ # https://github.com/lostisland/faraday/commit/63cf47c95b573539f047c729bd9ad67560bc83ff
88
+ def ssl_verify_peer(cert_string)
89
+ cert = nil
90
+ begin
91
+ cert = OpenSSL::X509::Certificate.new(cert_string)
92
+ rescue OpenSSL::X509::CertificateError => e
93
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate: #{e.message}"
94
+ return false
95
+ end
96
+
97
+ @last_seen_cert = cert
98
+
99
+ if certificate_store.verify(@last_seen_cert)
100
+ begin
101
+ certificate_store.add_cert(@last_seen_cert)
102
+ rescue OpenSSL::X509::StoreError => e
103
+ unless e.message == 'cert already in hash table'
104
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate: #{e.message}"
105
+ return false
106
+ end
107
+ end
108
+ true
109
+ else
110
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate"
111
+ false
112
+ end
113
+ end
114
+
115
+ def ssl_handshake_completed
116
+ unless OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, host)
117
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate"
118
+ false
119
+ else
120
+ true
121
+ end
122
+ end
123
+
124
+ def certificate_store
125
+ @certificate_store ||= OpenSSL::X509::Store.new.tap do |store|
126
+ store.set_default_paths
127
+ end
128
+ end
129
+
80
130
  # True if socket connection is ready to be released
81
131
  # i.e. it is not currently connecting or connected
82
132
  def ready_for_release?
@@ -106,6 +156,12 @@ module Ably::Realtime
106
156
  @connection
107
157
  end
108
158
 
159
+ def disconnect_with_reason(reason)
160
+ client.logger.error { "WebsocketTransport: Disconnecting due to error: #{reason}" }
161
+ @reason_closed = reason
162
+ disconnect
163
+ end
164
+
109
165
  def reason_closed
110
166
  @reason_closed
111
167
  end
@@ -214,6 +270,16 @@ module Ably::Realtime
214
270
  end
215
271
  )
216
272
  end
273
+
274
+ # TLS options to pass to EventMachine::Connection#start_tls
275
+ #
276
+ # See https://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection#start_tls-instance_method
277
+ def tls_opts
278
+ {
279
+ sni_hostname: host,
280
+ verify_peer: true,
281
+ }
282
+ end
217
283
  end
218
284
  end
219
285
  end
@@ -292,7 +292,7 @@ module Ably
292
292
  def internet_up?
293
293
  url = "http#{'s' if client.use_tls?}:#{Ably::INTERNET_CHECK.fetch(:url)}"
294
294
  EventMachine::DefaultDeferrable.new.tap do |deferrable|
295
- EventMachine::HttpRequest.new(url).get.tap do |http|
295
+ EventMachine::HttpRequest.new(url, tls: { verify_peer: true }).get.tap do |http|
296
296
  http.errback do
297
297
  yield false if block_given?
298
298
  deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED)
@@ -431,10 +431,10 @@ module Ably
431
431
  client.auth.auth_params.tap do |auth_deferrable|
432
432
  auth_deferrable.callback do |auth_params|
433
433
  url_params = auth_params.merge(
434
- format: client.protocol,
435
- echo: client.echo_messages,
436
- v: Ably::PROTOCOL_VERSION,
437
- lib: client.rest_client.lib_version_id,
434
+ 'format' => client.protocol,
435
+ 'echo' => client.echo_messages,
436
+ 'v' => Ably::PROTOCOL_VERSION,
437
+ 'agent' => client.rest_client.agent
438
438
  )
439
439
 
440
440
  # Use native websocket heartbeats if possible, but allow Ably protocol heartbeats
@@ -445,6 +445,7 @@ module Ably
445
445
  end
446
446
 
447
447
  url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
448
+ url_params.merge!(client.transport_params)
448
449
 
449
450
  if connection_resumable?
450
451
  url_params.merge! resume: key, connection_serial: serial
@@ -278,28 +278,14 @@ module Ably::Realtime
278
278
 
279
279
  # Return the presence messages history for the channel
280
280
  #
281
- # Once attached to a channel, you can retrieve presence message history on the channel before the
282
- # channel was attached with the option <tt>until_attach: true</tt>. This is very useful for
283
- # developers who wish to capture new presence events as well as retrieve historical presence state with
284
- # the guarantee that no presence history has been missed.
285
- #
286
281
  # @param (see Ably::Rest::Presence#history)
287
282
  # @option options (see Ably::Rest::Presence#history)
288
- # @option options [Boolean] :until_attach When true, request for history will be limited only to messages published before the associated channel was attached. The associated channel must be attached.
289
283
  #
290
284
  # @yield [Ably::Models::PaginatedResult<Ably::Models::PresenceMessage>] First {Ably::Models::PaginatedResult page} of {Ably::Models::PresenceMessage} objects accessible with {Ably::Models::PaginatedResult#items #items}.
291
285
  #
292
286
  # @return [Ably::Util::SafeDeferrable]
293
287
  #
294
288
  def history(options = {}, &callback)
295
- if options.delete(:until_attach)
296
- unless channel.attached?
297
- error = Ably::Exceptions::InvalidRequest.new('option :until_attach is invalid as the channel is not attached')
298
- return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
299
- end
300
- options[:from_serial] = channel.attached_serial
301
- end
302
-
303
289
  async_wrap(callback) do
304
290
  rest_presence.history(options.merge(async_blocking_operations: true))
305
291
  end
@@ -28,57 +28,60 @@ module Ably
28
28
  #
29
29
  # @param client [Ably::Rest::Client]
30
30
  # @param name [String] The name of the channel
31
- # @param channel_options [Hash] Channel options, currently reserved for Encryption options
32
- # @option channel_options [Hash,Ably::Models::CipherParams] :cipher A hash of options or a {Ably::Models::CipherParams} to configure the encryption. *:key* is required, all other options are optional. See {Ably::Util::Crypto#initialize} for a list of +:cipher+ options
31
+ # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions}
33
32
  #
34
33
  def initialize(client, name, channel_options = {})
35
34
  name = (ensure_utf_8 :name, name)
36
35
 
37
- update_options channel_options
36
+ @options = Ably::Models::ChannelOptions(channel_options)
38
37
  @client = client
39
38
  @name = name
40
39
  @push = PushChannel.new(self)
41
40
  end
42
41
 
43
- # Publish one or more messages to the channel.
44
- #
45
- # @param name [String, Array<Ably::Models::Message|Hash>, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
46
- # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
47
- # @param attributes [Hash, nil] Optional additional message attributes such as :extras, :id, :client_id or :connection_id, applied when name attribute is nil or a string
42
+ # Publish one or more messages to the channel. Five overloaded forms
43
+ # @param name [String, Array<Ably::Models::Message|Hash>, Ably::Models::Message, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs, or a single Ably::Model::Message object
44
+ # @param data [String, Array, Hash, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument, in which case an optional hash of query parameters
45
+ # @param attributes [Hash, nil] Optional additional message attributes such as :extras, :id, :client_id or :connection_id, applied when name attribute is nil or a string (Deprecated, will be removed in 2.0 in favour of constructing a Message object)
48
46
  # @return [Boolean] true if the message was published, otherwise false
49
47
  #
50
48
  # @example
51
- # # Publish a single message
49
+ # # Publish a single message with (name, data) form
52
50
  # channel.publish 'click', { x: 1, y: 2 }
53
51
  #
54
- # # Publish an array of message Hashes
52
+ # # Publish a single message with single Hash form
53
+ # message = { name: 'click', data: { x: 1, y: 2 } }
54
+ # channel.publish message
55
+ #
56
+ # # Publish an array of message Hashes form
55
57
  # messages = [
56
- # { name: 'click', { x: 1, y: 2 } },
57
- # { name: 'click', { x: 2, y: 3 } }
58
+ # { name: 'click', data: { x: 1, y: 2 } },
59
+ # { name: 'click', data: { x: 2, y: 3 } }
58
60
  # ]
59
61
  # channel.publish messages
60
62
  #
61
- # # Publish an array of Ably::Models::Message objects
63
+ # # Publish an array of Ably::Models::Message objects form
62
64
  # messages = [
63
- # Ably::Models::Message(name: 'click', { x: 1, y: 2 })
64
- # Ably::Models::Message(name: 'click', { x: 2, y: 3 })
65
+ # Ably::Models::Message(name: 'click', data: { x: 1, y: 2 })
66
+ # Ably::Models::Message(name: 'click', data: { x: 2, y: 3 })
65
67
  # ]
66
68
  # channel.publish messages
67
69
  #
70
+ # # Publish a single Ably::Models::Message object form
71
+ # message = Ably::Models::Message(name: 'click', data: { x: 1, y: 2 })
72
+ # channel.publish message
73
+ #
68
74
  def publish(name, data = nil, attributes = {})
69
- messages = if name.kind_of?(Enumerable)
70
- name
71
- else
72
- if name.kind_of?(Ably::Models::Message)
73
- raise ArgumentError, "name argument does not support single Message objects, only arrays of Message objects"
74
- end
75
+ qs_params = nil
76
+ qs_params = data if name.kind_of?(Enumerable) || name.kind_of?(Ably::Models::Message)
77
+
78
+ messages = build_messages(name, data, attributes) # (RSL1a, RSL1b)
75
79
 
76
- name = ensure_utf_8(:name, name, allow_nil: true)
77
- ensure_supported_payload data
78
- [{ name: name, data: data }.merge(attributes)]
80
+ if messages.sum(&:size) > (max_message_size = client.max_message_size || Ably::Rest::Client::MAX_MESSAGE_SIZE)
81
+ raise Ably::Exceptions::MaxMessageSizeExceeded.new("Maximum message size exceeded #{max_message_size} bytes.")
79
82
  end
80
83
 
81
- payload = messages.each_with_index.map do |message, index|
84
+ payload = messages.map do |message|
82
85
  Ably::Models::Message(message.dup).tap do |msg|
83
86
  msg.encode client.encoders, options
84
87
 
@@ -103,7 +106,8 @@ module Ably
103
106
  end
104
107
  end
105
108
 
106
- response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload)
109
+ options = qs_params ? { qs_params: qs_params } : {}
110
+ response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload, options)
107
111
 
108
112
  [201, 204].include?(response.status)
109
113
  end
@@ -149,12 +153,23 @@ module Ably
149
153
  @presence ||= Presence.new(client, self)
150
154
  end
151
155
 
152
- # @api private
153
- def update_options(channel_options)
154
- @options = channel_options.clone.freeze
156
+ # Sets or updates the stored channel options. (#RSL7)
157
+ # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions}
158
+ # @return [Ably::Models::ChannelOptions]
159
+ def set_options(channel_options)
160
+ @options = Ably::Models::ChannelOptions(channel_options)
161
+ end
162
+ alias options= set_options
163
+
164
+ # Makes GET request for channel details (#RSL8, #RSL8a)
165
+ #
166
+ # @return [Ably::Models::ChannelDetails]
167
+ def status
168
+ Ably::Models::ChannelDetails.new(client.get(base_path).body)
155
169
  end
156
170
 
157
171
  private
172
+
158
173
  def base_path
159
174
  "/channels/#{URI.encode_www_form_component(name)}"
160
175
  end
@@ -3,6 +3,9 @@ require 'json'
3
3
  require 'logger'
4
4
  require 'uri'
5
5
 
6
+ require 'typhoeus'
7
+ require 'faraday/typhoeus'
8
+
6
9
  require 'ably/rest/middleware/exceptions'
7
10
 
8
11
  module Ably
@@ -22,6 +25,9 @@ module Ably
22
25
  # Default Ably domain for REST
23
26
  DOMAIN = 'rest.ably.io'
24
27
 
28
+ MAX_MESSAGE_SIZE = 65536 # See spec TO3l8
29
+ MAX_FRAME_SIZE = 524288 # See spec TO3l8
30
+
25
31
  # Configuration for HTTP timeouts and HTTP request reattempts to fallback hosts
26
32
  HTTP_DEFAULTS = {
27
33
  open_timeout: 4,
@@ -32,6 +38,13 @@ module Ably
32
38
 
33
39
  FALLBACK_RETRY_TIMEOUT = 10 * 60
34
40
 
41
+ # Faraday 1.0 introduced new error types, however we want to support Faraday <1 too which only used Faraday::ClientError
42
+ FARADAY_CLIENT_OR_SERVER_ERRORS = if defined?(Faraday::ParsingError)
43
+ [Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed, Faraday::SSLError, Faraday::ParsingError]
44
+ else
45
+ Faraday::ClientError
46
+ end
47
+
35
48
  def_delegators :auth, :client_id, :auth_options
36
49
 
37
50
  # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
@@ -42,6 +55,10 @@ module Ably
42
55
  # @return [Symbol]
43
56
  attr_reader :protocol
44
57
 
58
+ # Client agent i.e. `example-gem/1.2.0 ably-ruby/1.1.5 ruby/3.1.1`
59
+ # @return [String]
60
+ attr_reader :agent
61
+
45
62
  # {Ably::Auth} authentication object configured for this connection
46
63
  # @return [Ably::Auth]
47
64
  attr_reader :auth
@@ -102,6 +119,14 @@ module Ably
102
119
  # @return [Boolean]
103
120
  attr_reader :idempotent_rest_publishing
104
121
 
122
+ # Max message size (TO2, TO3l8) by default (65536 bytes) 64KiB
123
+ # @return [Integer]
124
+ attr_reader :max_message_size
125
+
126
+ # Max frame size (TO2, TO3l8) by default (524288 bytes) 512KiB
127
+ # @return [Integer]
128
+ attr_reader :max_frame_size
129
+
105
130
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
106
131
  #
107
132
  # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key or Token ID
@@ -114,7 +139,7 @@ module Ably
114
139
  # @option options [Symbol] :protocol (:msgpack) Protocol used to communicate with Ably, :json and :msgpack currently supported
115
140
  # @option options [Boolean] :use_binary_protocol (true) When true will use the MessagePack binary protocol, when false it will use JSON encoding. This option will overide :protocol option
116
141
  # @option options [Logger::Severity,Symbol] :log_level (Logger::WARN) Log level for the standard Logger that outputs to STDOUT. Can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none
117
- # @option options [Logger] :logger A custom logger can be used however it must adhere to the Ruby Logger interface, see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
142
+ # @option options [Logger] :logger A custom logger can be used however it must adhere to the Ruby Logger interface, see http://www.ruby-doc.org/stdlib-3.1.1/libdoc/logger/rdoc/Logger.html
118
143
  # @option options [String] :client_id client ID identifying this connection to other clients
119
144
  # @option options [String] :auth_url a URL to be used to GET or POST a set of token request params, to obtain a signed token request
120
145
  # @option options [Hash] :auth_headers a set of application-specific headers to be added to any request made to the +auth_url+
@@ -122,7 +147,7 @@ module Ably
122
147
  # @option options [Symbol] :auth_method (:get) HTTP method to use with +auth_url+, must be either +:get+ or +:post+
123
148
  # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required.
124
149
  # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
125
- # @option options [Boolean] :query_time when true will query the {https://www.ably.io Ably} system for the current time instead of using the local time
150
+ # @option options [Boolean] :query_time when true will query the {https://www.ably.com Ably} system for the current time instead of using the local time
126
151
  # @option options [Hash] :default_token_params convenience to pass in +token_params+ that will be used as a default for all token requests. See {Auth#create_token_request}
127
152
  #
128
153
  # @option options [Integer] :http_open_timeout (4 seconds) timeout in seconds for opening an HTTP connection for all HTTP requests
@@ -136,6 +161,8 @@ module Ably
136
161
  #
137
162
  # @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request.
138
163
  # @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST
164
+ # @option options [Integer] :max_message_size (65536 bytes) Maximum size of all messages when publishing via REST publish()
165
+ # @option options [Integer] :max_frame_size (524288 bytes) Maximum size of frame
139
166
  #
140
167
  # @return [Ably::Rest::Client]
141
168
  #
@@ -158,6 +185,7 @@ module Ably
158
185
  end
159
186
  end
160
187
 
188
+ @agent = options.delete(:agent) || Ably::AGENT
161
189
  @realtime_client = options.delete(:realtime_client)
162
190
  @tls = options.delete(:tls) == false ? false : true
163
191
  @environment = options.delete(:environment) # nil is production
@@ -171,19 +199,25 @@ module Ably
171
199
  @custom_tls_port = options.delete(:tls_port)
172
200
  @add_request_ids = options.delete(:add_request_ids)
173
201
  @log_retries_as_info = options.delete(:log_retries_as_info)
174
- @idempotent_rest_publishing = options.delete(:idempotent_rest_publishing) || Ably.major_minor_version_numeric > 1.1
202
+ @max_message_size = options.delete(:max_message_size) || MAX_MESSAGE_SIZE
203
+ @max_frame_size = options.delete(:max_frame_size) || MAX_FRAME_SIZE
175
204
 
205
+ if (@idempotent_rest_publishing = options.delete(:idempotent_rest_publishing)).nil?
206
+ @idempotent_rest_publishing = Ably::PROTOCOL_VERSION.to_f > 1.1
207
+ end
176
208
 
177
- if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
178
- raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
209
+ if options[:fallback_hosts_use_default] && options[:fallback_hosts]
210
+ raise ArgumentError, "fallback_hosts_use_default cannot be set to try when fallback_hosts is also provided"
179
211
  end
180
212
  @fallback_hosts = case
181
213
  when options.delete(:fallback_hosts_use_default)
182
214
  Ably::FALLBACK_HOSTS
183
215
  when options_fallback_hosts = options.delete(:fallback_hosts)
184
216
  options_fallback_hosts
185
- when environment || custom_host || options[:realtime_host] || custom_port || custom_tls_port
217
+ when custom_host || options[:realtime_host] || custom_port || custom_tls_port
186
218
  []
219
+ when environment
220
+ CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES.map { |host| "#{environment}#{host}" }
187
221
  else
188
222
  Ably::FALLBACK_HOSTS
189
223
  end
@@ -195,6 +229,8 @@ module Ably
195
229
  @http_defaults = HTTP_DEFAULTS.dup
196
230
  options.each do |key, val|
197
231
  if http_key = key[/^http_(.+)/, 1]
232
+ # Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout)
233
+ val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001)
198
234
  @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym)
199
235
  end
200
236
  end
@@ -336,14 +372,17 @@ module Ably
336
372
  #
337
373
  # @return [Ably::Models::HttpPaginatedResponse<>]
338
374
  def request(method, path, params = {}, body = nil, headers = {}, options = {})
339
- raise "Method #{method.to_s.upcase} not supported" unless [:get, :put, :post].include?(method.to_sym)
375
+ raise "Method #{method.to_s.upcase} not supported" unless %i(get put patch post delete).include?(method.to_sym)
340
376
 
341
377
  response = case method.to_sym
342
- when :get
378
+ when :get, :delete
343
379
  reauthorize_on_authorization_failure do
344
380
  send_request(method, path, params, headers: headers)
345
381
  end
346
- when :post
382
+ when :post, :patch, :put
383
+ if body.to_json.bytesize > max_frame_size
384
+ raise Ably::Exceptions::MaxFrameSizeExceeded.new("Maximum frame size exceeded #{max_frame_size} bytes.")
385
+ end
347
386
  path_with_params = Addressable::URI.new
348
387
  path_with_params.query_values = params || {}
349
388
  query = path_with_params.query
@@ -459,16 +498,6 @@ module Ably
459
498
  end
460
499
  end
461
500
 
462
- # Library Ably version user agent
463
- # @api private
464
- def lib_version_id
465
- @lib_version_id ||= [
466
- 'ruby',
467
- Ably.lib_variant,
468
- Ably::VERSION
469
- ].compact.join('-')
470
- end
471
-
472
501
  # Allowable duration for an external auth request
473
502
  # For REST client this defaults to request_timeout
474
503
  # For Realtime clients this defaults to 250ms less than the realtime_request_timeout
@@ -559,12 +588,14 @@ module Ably
559
588
  request.options.context = {} if request.options.context.nil?
560
589
  request.options.context[:request_id] = request_id
561
590
  end
591
+ if options[:qs_params]
592
+ request.params.merge!(options[:qs_params])
593
+ end
562
594
  unless options[:send_auth_header] == false
563
595
  request.headers[:authorization] = auth.auth_header
564
- if options[:headers]
565
- options[:headers].map do |key, val|
566
- request.headers[key] = val
567
- end
596
+
597
+ options[:headers].to_h.merge(auth.extra_auth_headers).map do |key, val|
598
+ request.headers[key] = val
568
599
  end
569
600
  end
570
601
  end.tap do
@@ -578,7 +609,7 @@ module Ably
578
609
  end
579
610
  end
580
611
 
581
- rescue Faraday::TimeoutError, Faraday::ClientError, Ably::Exceptions::ServerError => error
612
+ rescue *([Faraday::TimeoutError, Ably::Exceptions::ServerError] + FARADAY_CLIENT_OR_SERVER_ERRORS) => error
582
613
  retry_sequence_id ||= SecureRandom.urlsafe_base64(4)
583
614
  time_passed = Time.now - requested_at
584
615
 
@@ -598,7 +629,7 @@ module Ably
598
629
  case error
599
630
  when Faraday::TimeoutError
600
631
  raise Ably::Exceptions::ConnectionTimeout.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT, error, { request_id: request_id })
601
- when Faraday::ClientError
632
+ when *FARADAY_CLIENT_OR_SERVER_ERRORS
602
633
  # request_id is also available in the request context
603
634
  raise Ably::Exceptions::ConnectionError.new(error.message, nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error, { request_id: request_id })
604
635
  else
@@ -647,7 +678,7 @@ module Ably
647
678
  accept: mime_type,
648
679
  user_agent: user_agent,
649
680
  'X-Ably-Version' => Ably::PROTOCOL_VERSION,
650
- 'X-Ably-Lib' => lib_version_id
681
+ 'Ably-Agent' => agent
651
682
  },
652
683
  request: {
653
684
  open_timeout: http_defaults.fetch(:open_timeout),
@@ -656,7 +687,7 @@ module Ably
656
687
  }
657
688
  end
658
689
 
659
- # Return a Faraday middleware stack to initiate the Faraday::Connection with
690
+ # Return a Faraday middleware stack to initiate the Faraday::RackBuilder with
660
691
  #
661
692
  # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
662
693
  def middleware
@@ -668,8 +699,8 @@ module Ably
668
699
 
669
700
  setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
670
701
 
671
- # Set Faraday's HTTP adapter
672
- builder.adapter :excon
702
+ # Set Faraday's HTTP adapter with support for HTTP/2
703
+ builder.adapter :typhoeus, http_version: :httpv2_0
673
704
  end
674
705
  end
675
706