ably-pubsub-core 0.0.1.pre

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ably/agent.rb +8 -0
  3. data/lib/ably/auth.rb +791 -0
  4. data/lib/ably/exceptions.rb +169 -0
  5. data/lib/ably/logger.rb +119 -0
  6. data/lib/ably/models/auth_details.rb +45 -0
  7. data/lib/ably/models/channel_details.rb +69 -0
  8. data/lib/ably/models/channel_metrics.rb +107 -0
  9. data/lib/ably/models/channel_occupancy.rb +50 -0
  10. data/lib/ably/models/channel_options.rb +120 -0
  11. data/lib/ably/models/channel_state_change.rb +84 -0
  12. data/lib/ably/models/channel_status.rb +63 -0
  13. data/lib/ably/models/cipher_params.rb +137 -0
  14. data/lib/ably/models/connection_details.rb +149 -0
  15. data/lib/ably/models/connection_state_change.rb +83 -0
  16. data/lib/ably/models/delta_extras.rb +28 -0
  17. data/lib/ably/models/device_details.rb +126 -0
  18. data/lib/ably/models/device_push_details.rb +94 -0
  19. data/lib/ably/models/error_info.rb +104 -0
  20. data/lib/ably/models/http_paginated_response.rb +112 -0
  21. data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
  22. data/lib/ably/models/message.rb +223 -0
  23. data/lib/ably/models/message_encoders/base.rb +135 -0
  24. data/lib/ably/models/message_encoders/base64.rb +40 -0
  25. data/lib/ably/models/message_encoders/cipher.rb +84 -0
  26. data/lib/ably/models/message_encoders/json.rb +34 -0
  27. data/lib/ably/models/message_encoders/utf8.rb +26 -0
  28. data/lib/ably/models/nil_logger.rb +20 -0
  29. data/lib/ably/models/paginated_result.rb +201 -0
  30. data/lib/ably/models/presence_message.rb +244 -0
  31. data/lib/ably/models/protocol_message.rb +288 -0
  32. data/lib/ably/models/push_channel_subscription.rb +114 -0
  33. data/lib/ably/models/stats.rb +242 -0
  34. data/lib/ably/models/stats_types.rb +107 -0
  35. data/lib/ably/models/token_details.rb +148 -0
  36. data/lib/ably/models/token_request.rb +180 -0
  37. data/lib/ably/modules/ably.rb +26 -0
  38. data/lib/ably/modules/async_wrapper.rb +69 -0
  39. data/lib/ably/modules/channels_collection.rb +105 -0
  40. data/lib/ably/modules/conversions.rb +153 -0
  41. data/lib/ably/modules/encodeable.rb +111 -0
  42. data/lib/ably/modules/enum.rb +230 -0
  43. data/lib/ably/modules/event_emitter.rb +200 -0
  44. data/lib/ably/modules/event_machine_helpers.rb +26 -0
  45. data/lib/ably/modules/exception_codes.rb +128 -0
  46. data/lib/ably/modules/http_helpers.rb +41 -0
  47. data/lib/ably/modules/message_emitter.rb +78 -0
  48. data/lib/ably/modules/message_pack.rb +14 -0
  49. data/lib/ably/modules/model_common.rb +84 -0
  50. data/lib/ably/modules/safe_deferrable.rb +83 -0
  51. data/lib/ably/modules/safe_yield.rb +41 -0
  52. data/lib/ably/modules/state_emitter.rb +181 -0
  53. data/lib/ably/modules/state_machine.rb +59 -0
  54. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  55. data/lib/ably/modules/uses_state_machine.rb +111 -0
  56. data/lib/ably/realtime/auth.rb +283 -0
  57. data/lib/ably/realtime/channel/channel_manager.rb +272 -0
  58. data/lib/ably/realtime/channel/channel_properties.rb +40 -0
  59. data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
  60. data/lib/ably/realtime/channel/publisher.rb +82 -0
  61. data/lib/ably/realtime/channel/push_channel.rb +69 -0
  62. data/lib/ably/realtime/channel.rb +440 -0
  63. data/lib/ably/realtime/channels.rb +71 -0
  64. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
  65. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
  66. data/lib/ably/realtime/client.rb +363 -0
  67. data/lib/ably/realtime/connection/connection_manager.rb +594 -0
  68. data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
  69. data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
  70. data/lib/ably/realtime/connection.rb +738 -0
  71. data/lib/ably/realtime/models/nil_channel.rb +30 -0
  72. data/lib/ably/realtime/presence/members_map.rb +395 -0
  73. data/lib/ably/realtime/presence/presence_manager.rb +78 -0
  74. data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
  75. data/lib/ably/realtime/presence.rb +502 -0
  76. data/lib/ably/realtime/push/admin.rb +75 -0
  77. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  78. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  79. data/lib/ably/realtime/push.rb +25 -0
  80. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  81. data/lib/ably/realtime.rb +60 -0
  82. data/lib/ably/rest/channel/push_channel.rb +59 -0
  83. data/lib/ably/rest/channel.rb +194 -0
  84. data/lib/ably/rest/channels.rb +47 -0
  85. data/lib/ably/rest/client.rb +727 -0
  86. data/lib/ably/rest/middleware/encoder.rb +49 -0
  87. data/lib/ably/rest/middleware/exceptions.rb +53 -0
  88. data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  89. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
  90. data/lib/ably/rest/middleware/logger.rb +58 -0
  91. data/lib/ably/rest/middleware/parse_json.rb +27 -0
  92. data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
  93. data/lib/ably/rest/presence.rb +113 -0
  94. data/lib/ably/rest/push/admin.rb +58 -0
  95. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  96. data/lib/ably/rest/push/device_registrations.rb +114 -0
  97. data/lib/ably/rest/push.rb +25 -0
  98. data/lib/ably/rest.rb +40 -0
  99. data/lib/ably/util/ably_extensions.rb +29 -0
  100. data/lib/ably/util/crypto.rb +134 -0
  101. data/lib/ably/util/pub_sub.rb +45 -0
  102. data/lib/ably/util/safe_deferrable.rb +44 -0
  103. data/lib/ably/version.rb +7 -0
  104. data/lib/ably.rb +16 -0
  105. metadata +497 -0
data/lib/ably/auth.rb ADDED
@@ -0,0 +1,791 @@
1
+ require 'json'
2
+ require 'faraday'
3
+ require 'securerandom'
4
+
5
+ require 'ably/rest/middleware/external_exceptions'
6
+
7
+ module Ably
8
+ # Creates Ably {Ably::Models::TokenRequest} objects and obtains Ably Tokens from Ably to subsequently issue to less trusted clients.
9
+ #
10
+ class Auth
11
+ include Ably::Modules::Conversions
12
+ include Ably::Modules::HttpHelpers
13
+
14
+ # Default capability Hash object and TTL in seconds for issued tokens
15
+ TOKEN_DEFAULTS = {
16
+ renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER)
17
+ }.freeze
18
+
19
+ API_KEY_REGEX = /^[\w-]{2,}\.[\w-]{2,}:[\w-]{2,}$/
20
+
21
+ # Supported AuthOption keys, see https://www.ably.com/docs/realtime/types#auth-options
22
+ # TODO: Review client_id usage embedded incorrectly within AuthOptions.
23
+ # This is legacy code to configure a client with a client_id from the ClientOptions
24
+ # TODO: Review inclusion of use_token_auth, ttl, token_params in auth options
25
+ AUTH_OPTIONS_KEYS = %w(
26
+ auth_callback
27
+ auth_url
28
+ auth_method
29
+ auth_headers
30
+ auth_params
31
+ client_id
32
+ key
33
+ key_name
34
+ key_secret
35
+ query_time
36
+ token
37
+ token_details
38
+ token_params
39
+ ttl
40
+ use_token_auth
41
+ )
42
+
43
+ attr_reader :options, :token_params, :current_token_details
44
+ alias_method :auth_options, :options
45
+
46
+ # Creates an Auth object
47
+ #
48
+ # @param [Ably::Rest::Client] client {Ably::Rest::Client} this Auth object uses
49
+ # @param [Hash] token_params the token params used as a default for future token requests
50
+ # @param [Hash] auth_options the authentication options used as a default future token requests
51
+ # @option (see #request_token)
52
+ #
53
+ def initialize(client, token_params, auth_options)
54
+ unless auth_options.kind_of?(Hash)
55
+ raise ArgumentError, 'Expected auth_options to be a Hash'
56
+ end
57
+
58
+ unless token_params.kind_of?(Hash)
59
+ raise ArgumentError, 'Expected token_params to be a Hash'
60
+ end
61
+
62
+ # Ensure instance variables are defined
63
+ @client_id = nil
64
+ @client_id_validated = nil
65
+
66
+ ensure_valid_auth_attributes auth_options
67
+
68
+ @client = client
69
+ @options = auth_options.dup
70
+ @token_params = token_params.dup
71
+ @token_option = options[:token] || options[:token_details]
72
+
73
+ if options[:key] && (options[:key_secret] || options[:key_name])
74
+ raise ArgumentError, 'key and key_name or key_secret are mutually exclusive. Provider either a key or key_name & key_secret'
75
+ end
76
+
77
+ split_api_key_into_key_and_secret! options if options[:key]
78
+ store_and_delete_basic_auth_key_from_options! options
79
+
80
+ if using_basic_auth? && !api_key_present?
81
+ raise ArgumentError, 'key is missing. Either an API key, token, or token auth method must be provided'
82
+ end
83
+
84
+ if options[:client_id] == '*'
85
+ raise ArgumentError, 'A client cannot be configured with a wildcard client_id, only a token can have a wildcard client_id privilege'
86
+ end
87
+
88
+ if has_client_id? && !token_creatable_externally? && !token_option
89
+ @client_id = ensure_utf_8(:client_id, client_id) if client_id
90
+ end
91
+
92
+ # If a token details object or token string is provided in the initializer
93
+ # then the client can be authorized immediately using this token
94
+ if token_option
95
+ token_details = convert_to_token_details(token_option)
96
+ if token_details
97
+ begin
98
+ token_details = authorize_with_token(token_details)
99
+ logger.debug { "Auth: new token passed in to the initializer: #{token_details}" }
100
+ rescue StandardError => e
101
+ logger.error { "Auth: Implicit authorization using the provided token failed: #{e}" }
102
+ end
103
+ end
104
+ end
105
+
106
+ @options.freeze
107
+ @token_params.freeze
108
+ end
109
+
110
+ # Instructs the library to get a new token immediately. When using the realtime client, it upgrades the current
111
+ # realtime connection to use the new token, or if not connected, initiates a connection to Ably, once the new token
112
+ # has been obtained. Also stores any `token_params` and `auth_options` passed
113
+ # in as the new defaults, to be used for all subsequent implicit or explicit token requests. Any `token_params`
114
+ # and `auth_options` objects passed in entirely replace, as opposed to being merged with, the current client library saved values.
115
+ #
116
+ # @spec RSA10
117
+ #
118
+ # @param [Hash, nil] token_params the token params used for future token requests. When nil, previously configured token params are used
119
+ # @param [Hash, nil] auth_options the authentication options used for future token requests. When nil, previously configure authentication options are used
120
+ # @option (see #request_token)
121
+ #
122
+ # @return (see #create_token_request)
123
+ #
124
+ # @example
125
+ # # will issue a simple token request using basic auth
126
+ # client = Ably::Rest::Client.new(key: 'key.id:secret')
127
+ # token_details = client.auth.authorize
128
+ #
129
+ # # will use token request from block to authorize if not already authorized
130
+ # token_details = client.auth.authorize {}, auth_callback: lambda do |token_parmas|
131
+ # # create token_request object
132
+ # token_request
133
+ # end
134
+ #
135
+ def authorize(token_params = nil, auth_options = nil)
136
+ if auth_options.nil?
137
+ auth_options = options # Use default options
138
+
139
+ if options.has_key?(:query_time)
140
+ @options = options.dup
141
+ # Query the server time only happens once
142
+ # the options remain in auth_options though so they are passed to request_token
143
+ @options.delete(:query_time)
144
+ @options.freeze
145
+ end
146
+ else
147
+ ensure_valid_auth_attributes auth_options
148
+
149
+ auth_options = auth_options.dup
150
+
151
+ if auth_options[:token_params]
152
+ token_params = auth_options.delete(:token_params).merge(token_params || {})
153
+ end
154
+
155
+ # If basic credentials are provided then overwrite existing options
156
+ # otherwise we need to retain the existing credentials in the auth options
157
+ split_api_key_into_key_and_secret! auth_options if auth_options[:key]
158
+ if auth_options[:key_name] && auth_options[:key_secret]
159
+ store_and_delete_basic_auth_key_from_options! auth_options
160
+ end
161
+
162
+ @options = auth_options.dup
163
+
164
+ # Query the server time only happens once
165
+ # the options remain in auth_options though so they are passed to request_token
166
+ @options.delete(:query_time)
167
+
168
+ @options.freeze
169
+ end
170
+
171
+ # Unless provided, defaults are used
172
+ unless token_params.nil?
173
+ @token_params = token_params.dup
174
+ # Timestamp is only valid for this request
175
+ @token_params.delete(:timestamp)
176
+ @token_params.freeze
177
+ end
178
+
179
+ authorize_with_token(request_token(token_params || @token_params, auth_options)).tap do |new_token_details|
180
+ logger.debug { "Auth: new token following authorisation: #{new_token_details}" }
181
+
182
+ # If authorize the realtime library required auth, then yield the token in a block
183
+ if block_given?
184
+ yield new_token_details
185
+ end
186
+ end
187
+ end
188
+
189
+ # @deprecated Use {#authorize} instead
190
+ def authorise(*args, &block)
191
+ logger.warn { "Auth#authorise is deprecated and will be removed in 1.0. Please use Auth#authorize instead" }
192
+ authorize(*args, &block)
193
+ end
194
+
195
+ # Calls the requestToken REST API endpoint to obtain an Ably Token according to the specified `token_params` and `auth_options`.
196
+ # Both `token_params` and `auth_options` are optional. When omitted or null, the default token parameters and authentication
197
+ # options for the client library are used, as specified in the `client_options` when the client library was instantiated,
198
+ # or later updated with an explicit authorize request. Values passed in are used instead of, rather than being merged with, the default values.
199
+ # To understand why an Ably {Ably::Models::TokenRequest} may be issued to clients in favor of a token, see Token Authentication explained.
200
+ #
201
+ # @spec RSA8e
202
+ #
203
+ # @param [Hash] auth_options (see #create_token_request)
204
+ # @option auth_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
205
+ # @option auth_options [Hash] :auth_headers a set of application-specific headers to be added to any request made to the +auth_url+
206
+ # @option auth_options [Hash] :auth_params a set of application-specific query params to be added to any request made to the +auth_url+
207
+ # @option auth_options [Symbol] :auth_method (:get) HTTP method to use with +auth_url+, must be either +:get+ or +:post+
208
+ # @option auth_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.
209
+ # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
210
+ # @param [Hash] token_params (see #create_token_request)
211
+ # @option (see #create_token_request)
212
+ #
213
+ # @return [Ably::Models::TokenDetails] A {Ably::Models::TokenDetails} object. RSA16
214
+ #
215
+ # @example
216
+ # # simple token request using basic auth
217
+ # client = Ably::Rest::Client.new(key: 'key.id:secret')
218
+ # token_details = client.auth.request_token
219
+ #
220
+ # # token request with token params
221
+ # client.auth.request_token ttl: 1.hour
222
+ #
223
+ # # token request using auth block
224
+ # token_details = client.auth.request_token {}, auth_callback: lambda do |token_params|
225
+ # # create token_request object
226
+ # token_request
227
+ # end
228
+ #
229
+ def request_token(token_params = {}, auth_options = {})
230
+ ensure_valid_auth_attributes auth_options
231
+
232
+ # Token param precedence (lowest to highest):
233
+ # Auth default => client_id => auth_options[:token_params] arg => token_params arg
234
+ token_params = self.token_params.merge(
235
+ (client_id ? { client_id: client_id } : {}).
236
+ merge(auth_options[:token_params] || {}).
237
+ merge(token_params)
238
+ )
239
+
240
+ auth_options = self.options.merge(auth_options)
241
+
242
+ token_request = if auth_callback = auth_options.delete(:auth_callback)
243
+ begin
244
+ Timeout::timeout(client.auth_request_timeout) do
245
+ auth_callback.call(token_params)
246
+ end
247
+ rescue StandardError => err
248
+ raise Ably::Exceptions::AuthenticationFailed.new("auth_callback failed: #{err.message}", nil, nil, err, fallback_status: 500, fallback_code: Ably::Exceptions::Codes::CONNECTION_NOT_ESTABLISHED_NO_TRANSPORT_HANDLE)
249
+ end
250
+ elsif auth_url = auth_options.delete(:auth_url)
251
+ begin
252
+ Timeout::timeout(client.auth_request_timeout) do
253
+ token_request_from_auth_url(auth_url, auth_options, token_params)
254
+ end
255
+ rescue StandardError => err
256
+ raise Ably::Exceptions::AuthenticationFailed.new("auth_url failed: #{err.message}", nil, nil, err, fallback_status: 500, fallback_code: Ably::Exceptions::Codes::CONNECTION_NOT_ESTABLISHED_NO_TRANSPORT_HANDLE)
257
+ end
258
+ else
259
+ create_token_request(token_params, auth_options)
260
+ end
261
+
262
+ convert_to_token_details(token_request).tap do |token_details|
263
+ return token_details if token_details
264
+ end
265
+
266
+ send_token_request(token_request)
267
+ end
268
+
269
+ # Creates and signs an {Ably::Models::TokenRequest} based on the specified (or if none specified, the client
270
+ # library stored) `token_params` and `auth_options`. Note this can only be used when the API key value is available
271
+ # locally. Otherwise, the {Ably::Models::TokenRequest} must be obtained from the key owner. Use this to generate
272
+ # an {Ably::Models::TokenRequest} in order to implement an Ably Token request callback for use by other clients.
273
+ # Both `token_params` and `auth_options` are optional. When omitted or null, the default token parameters
274
+ # and authentication options for the client library are used, as specified in the `client_options` when the client
275
+ # library was instantiated, or later updated with an explicit authorize request. Values passed in are used instead
276
+ # of, rather than being merged with, the default values. To understand why an {Ably::Models::TokenRequest} may be
277
+ # issued to clients in favor of a token, see Token Authentication explained.
278
+ #
279
+ # @spec RSA9
280
+ #
281
+ # @param [Hash] token_params the token params used in the token request
282
+ # @option token_params [String] :client_id A client ID to associate with this token. The generated token may be used to authenticate as this +client_id+
283
+ # @option token_params [Integer] :ttl validity time in seconds for the requested {Ably::Models::TokenDetails}. Limits may apply, see {https://www.ably.com/docs/general/authentication}
284
+ # @option token_params [Hash] :capability canonicalised representation of the resource paths and associated operations
285
+ # @option token_params [Time] :timestamp the time of the request
286
+ # @option token_params [String] :nonce an unquoted, unescaped random string of at least 16 characters
287
+ #
288
+ # @param [Hash] auth_options the authentication options for the token request
289
+ # @option auth_options [String] :key API key comprising the key name and key secret in a single string
290
+ # @option auth_options [String] :client_id client ID identifying this connection to other clients (will use +client_id+ specified when library was instanced if provided)
291
+ # @option auth_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
292
+ # @option auth_options [Hash] :token_params convenience to pass in +token_params+ within the +auth_options+ argument, especially useful when setting default token_params in the client constructor
293
+ #
294
+ # @return [Models::TokenRequest]
295
+ #
296
+ # @example
297
+ # client.auth.create_token_request({ ttl: 3600 }, { id: 'asd.asd' })
298
+ # #<Ably::Models::TokenRequest:0x007fd5d919df78
299
+ # # @hash={
300
+ # # :id=>"asds.adsa",
301
+ # # :clientId=>nil,
302
+ # # :ttl=>3600000,
303
+ # # :timestamp=>1428973674000,
304
+ # # :capability=>"{\"*\":[\"*\"]}",
305
+ # # :nonce=>"95e543b88299f6bae83df9b12fbd1ecd",
306
+ # # :mac=>"881oZHeFo6oMim7....uE56a8gUxHw="
307
+ # # }
308
+ # #>>
309
+ def create_token_request(token_params = {}, auth_options = {})
310
+ ensure_valid_auth_attributes auth_options
311
+
312
+ auth_options = auth_options.dup
313
+ token_params = (auth_options[:token_params] || {}).merge(token_params)
314
+
315
+ split_api_key_into_key_and_secret! auth_options if auth_options[:key]
316
+ request_key_name = auth_options.delete(:key_name) || key_name
317
+ request_key_secret = auth_options.delete(:key_secret) || key_secret
318
+
319
+ raise Ably::Exceptions::TokenRequestFailed, 'Key Name and Key Secret are required to generate a new token request' unless request_key_name && request_key_secret
320
+
321
+ ensure_current_time_is_based_on_server_time if auth_options[:query_time]
322
+ timestamp = token_params.delete(:timestamp) || current_time
323
+ timestamp = Time.at(timestamp) if timestamp.kind_of?(Integer)
324
+
325
+
326
+
327
+ token_request = {
328
+ keyName: request_key_name,
329
+ timestamp: (timestamp.to_f * 1000).round,
330
+ nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8')
331
+ }
332
+
333
+ token_client_id = token_params[:client_id] || auth_options[:client_id] || client_id
334
+ token_request[:clientId] = token_client_id if token_client_id
335
+
336
+ if token_params[:ttl]
337
+ token_ttl = [
338
+ token_params[:ttl],
339
+ Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER + TOKEN_DEFAULTS.fetch(:renew_token_buffer) # never issue a token that will be immediately considered expired due to the buffer
340
+ ].max
341
+ token_request[:ttl] = (token_ttl * 1000).to_i
342
+ end
343
+
344
+ token_request[:capability] = token_params[:capability] if token_params[:capability]
345
+ if token_request[:capability].is_a?(Hash)
346
+ lexicographic_ordered_capabilities = Hash[
347
+ token_request[:capability].sort_by { |key, value| key }.map do |key, value|
348
+ [key, value.sort]
349
+ end
350
+ ]
351
+ token_request[:capability] = JSON.dump(lexicographic_ordered_capabilities)
352
+ end
353
+
354
+ token_request[:mac] = sign_params(token_request, request_key_secret)
355
+
356
+ # Undocumented feature to request a persisted token
357
+ token_request[:persisted] = token_params[:persisted] if token_params[:persisted]
358
+
359
+ Models::TokenRequest.new(token_request)
360
+ end
361
+
362
+ def key
363
+ "#{key_name}:#{key_secret}" if api_key_present?
364
+ end
365
+
366
+ def key_name
367
+ @key_name
368
+ end
369
+
370
+ def key_secret
371
+ @key_secret
372
+ end
373
+
374
+ # True when Basic Auth is being used to authenticate with Ably
375
+ def using_basic_auth?
376
+ !using_token_auth?
377
+ end
378
+
379
+ # True when Token Auth is being used to authenticate with Ably
380
+ def using_token_auth?
381
+ return options[:use_token_auth] if options.has_key?(:use_token_auth)
382
+ !!(token_option || current_token_details || token_creatable_externally?)
383
+ end
384
+
385
+ def client_id
386
+ @client_id || options[:client_id]
387
+ end
388
+
389
+ # When a client has authenticated with Ably and the client is either anonymous (cannot assume a +client_id+)
390
+ # or has an assigned +client_id+ (implicit in all operations), then this client has a validated +client_id+, even
391
+ # if that client_id is +nil+ (anonymous)
392
+ #
393
+ # Once validated by Ably, the client library will enforce the use of the +client_id+ identity provided by Ably, rejecting
394
+ # messages with an invalid +client_id+ immediately
395
+ #
396
+ # @return [Boolean]
397
+ def client_id_validated?
398
+ !!@client_id_validated
399
+ end
400
+
401
+ # Auth header string used in HTTP requests to Ably
402
+ # Will reauthorize implicitly if required and capable
403
+ #
404
+ # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header
405
+ def auth_header
406
+ if using_token_auth?
407
+ token_auth_header
408
+ else
409
+ basic_auth_header
410
+ end
411
+ end
412
+
413
+ # Extra headers that may be used during authentication
414
+ #
415
+ # @return [Hash] headers
416
+ def extra_auth_headers
417
+ if client_id_for_request
418
+ { 'X-Ably-ClientId' => Base64.urlsafe_encode64(client_id_for_request) }
419
+ else
420
+ {}
421
+ end
422
+ end
423
+
424
+ # ClientId that needs to be included with every rest/realtime request
425
+ # spec - RSA7e
426
+ # @return string
427
+ def client_id_for_request
428
+ options[:client_id]
429
+ end
430
+
431
+ # Auth params used in URI endpoint for Realtime connections
432
+ # Will reauthorize implicitly if required and capable
433
+ #
434
+ # @return [Hash] Auth params for a new Realtime connection
435
+ def auth_params
436
+ if using_token_auth?
437
+ token_auth_params
438
+ else
439
+ basic_auth_params
440
+ end
441
+ end
442
+
443
+ # True if prerequisites for creating a new token request are present
444
+ #
445
+ # One of the following criterion must be met:
446
+ # * Valid API key and token option not provided as token options cannot be determined
447
+ # * Authentication callback for new token requests
448
+ # * Authentication URL for new token requests
449
+ #
450
+ # @return [Boolean]
451
+ def token_renewable?
452
+ token_creatable_externally? || (api_key_present? && !token_option)
453
+ end
454
+
455
+ # Returns false when attempting to send an API Key over a non-secure connection
456
+ # Token auth must be used for non-secure connections
457
+ #
458
+ # @return [Boolean]
459
+ def authentication_security_requirements_met?
460
+ client.use_tls? || using_token_auth?
461
+ end
462
+
463
+ # True if token provided client_id is compatible with the client's configured +client_id+, when applicable
464
+ #
465
+ # @return [Boolean]
466
+ # @api private
467
+ def token_client_id_allowed?(token_client_id)
468
+ return true if client_id.nil? # no explicit client_id specified for this client
469
+ return true if client_id == '*' || token_client_id == '*' # wildcard supported always
470
+ token_client_id == client_id
471
+ end
472
+
473
+ # True if assumed_client_id is compatible with the client's configured or Ably assigned +client_id+
474
+ #
475
+ # @return [Boolean]
476
+ # @api private
477
+ def can_assume_client_id?(assumed_client_id)
478
+ if client_id_validated?
479
+ client_id == '*' || (client_id == assumed_client_id)
480
+ elsif !options[:client_id] || options[:client_id] == '*'
481
+ true # client ID is unknown
482
+ else
483
+ options[:client_id] == assumed_client_id
484
+ end
485
+ end
486
+
487
+ # Configures the client ID for this client
488
+ # Typically this occurs following an Auth or receiving a {Ably::Models::ProtocolMessage} with a +client_id+ in the {Ably::Models::ConnectionDetails}
489
+ #
490
+ # @api private
491
+ def configure_client_id(new_client_id)
492
+ if has_client_id?
493
+ # If new client ID from Ably is a wildcard, but preconfigured clientId is set, then keep the existing clientId
494
+ if new_client_id == "*"
495
+ @client_id_validated = true
496
+ return
497
+ end
498
+ # If client_id is defined and not a wildcard, prevent it changing, this is not supported
499
+ if new_client_id != client_id
500
+ raise Ably::Exceptions::IncompatibleClientId.new("Client ID is immutable once configured for a client. Client ID cannot be changed to '#{new_client_id}'")
501
+ end
502
+ end
503
+ @client_id_validated = true
504
+ @client_id = new_client_id
505
+ end
506
+
507
+ # True when a client_id other than a wildcard is configured for Auth
508
+ #
509
+ # @api private
510
+ def has_client_id?
511
+ client_id && (client_id != '*')
512
+ end
513
+
514
+ private
515
+ def client
516
+ @client
517
+ end
518
+
519
+ def token_option
520
+ @token_option
521
+ end
522
+
523
+ def authorize_when_necessary
524
+ if current_token_details && !current_token_details.expired?(from: current_time)
525
+ return current_token_details
526
+ end
527
+
528
+ authorize
529
+ end
530
+
531
+ # Returns the current device clock time unless the
532
+ # the server time has previously been requested with query_time: true
533
+ # and the @server_time_offset is configured
534
+ def current_time
535
+ if @server_time_offset
536
+ Time.now + @server_time_offset
537
+ else
538
+ Time.now
539
+ end
540
+ end
541
+
542
+ # Get the difference in time between the server
543
+ # and the local clock and store this for future time requests
544
+ def ensure_current_time_is_based_on_server_time
545
+ server_time = client.time
546
+ @server_time_offset = server_time.to_f - Time.now.to_f
547
+ end
548
+
549
+ def ensure_valid_auth_attributes(attributes)
550
+ (attributes.keys.map(&:to_s) - AUTH_OPTIONS_KEYS).tap do |unsupported_keys|
551
+ raise ArgumentError, "The key(s) #{unsupported_keys.map { |k| ":#{k}" }.join(', ')} are not valid AuthOptions" unless unsupported_keys.empty?
552
+ end
553
+
554
+ if attributes[:timestamp]
555
+ unless attributes[:timestamp].kind_of?(Time) || attributes[:timestamp].kind_of?(Numeric)
556
+ raise ArgumentError, ':timestamp must be a Time or positive Integer value of seconds since epoch'
557
+ end
558
+ end
559
+
560
+ if attributes[:ttl]
561
+ unless attributes[:ttl].kind_of?(Numeric) && attributes[:ttl].to_f > 0
562
+ raise ArgumentError, ':ttl must be a positive Numeric value representing time to live in seconds'
563
+ end
564
+ end
565
+
566
+ if attributes[:auth_headers]
567
+ unless attributes[:auth_headers].kind_of?(Hash)
568
+ raise ArgumentError, ':auth_headers must be a valid Hash'
569
+ end
570
+ end
571
+
572
+ if attributes[:auth_params]
573
+ unless attributes[:auth_params].kind_of?(Hash)
574
+ raise ArgumentError, ':auth_params must be a valid Hash'
575
+ end
576
+ end
577
+
578
+ if attributes[:auth_method]
579
+ unless %(get post).include?(attributes[:auth_method].to_s)
580
+ raise ArgumentError, ':auth_method must be either :get or :post'
581
+ end
582
+ end
583
+
584
+ if attributes[:auth_callback]
585
+ unless attributes[:auth_callback].respond_to?(:call)
586
+ raise ArgumentError, ':auth_callback must be a Proc'
587
+ end
588
+ end
589
+ end
590
+
591
+ def ensure_api_key_sent_over_secure_connection
592
+ raise Ably::Exceptions::InsecureRequest, 'Cannot use Basic Auth over non-TLS connections' unless authentication_security_requirements_met?
593
+ end
594
+
595
+ # Basic Auth HTTP Authorization header value
596
+ def basic_auth_header
597
+ ensure_api_key_sent_over_secure_connection
598
+ "Basic #{encode64("#{key}")}"
599
+ end
600
+
601
+ def split_api_key_into_key_and_secret!(options)
602
+ api_key_parts = options[:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/)
603
+ raise ArgumentError, 'key is invalid' unless api_key_parts
604
+
605
+ options[:key_name] = api_key_parts[:name].encode(Encoding::UTF_8)
606
+ options[:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8)
607
+
608
+ options.delete :key
609
+ end
610
+
611
+ def store_and_delete_basic_auth_key_from_options!(options)
612
+ @key_name = options.delete(:key_name)
613
+ @key_secret = options.delete(:key_secret)
614
+ end
615
+
616
+ # Returns the current token if it exists or authorizes and retrieves a token
617
+ def token_auth_string
618
+ if !current_token_details && token_option
619
+ logger.debug { "Auth: Token auth string missing, authorizing implicitly now" }
620
+ # A TokenRequest was configured in the ClientOptions +:token field+ and no current token exists
621
+ # Note: If a Token or TokenDetails is provided in the initializer, the token is stored in +current_token_details+
622
+ authorize_with_token send_token_request(token_option)
623
+ current_token_details.token
624
+ else
625
+ # Authorize will use the current token if one exists and is not expired, otherwise a new token will be issued
626
+ authorize_when_necessary.token
627
+ end
628
+ end
629
+
630
+ def configure_current_token_details(token_details)
631
+ @current_token_details = token_details
632
+ end
633
+
634
+ # Token Auth HTTP Authorization header value
635
+ def token_auth_header
636
+ "Bearer #{encode64(token_auth_string)}"
637
+ end
638
+
639
+ # Basic Auth params to authenticate the Realtime connection
640
+ def basic_auth_params
641
+ ensure_api_key_sent_over_secure_connection
642
+ {
643
+ key: key
644
+ }
645
+ end
646
+
647
+ # Token Auth params to authenticate the Realtime connection
648
+ def token_auth_params
649
+ {
650
+ access_token: token_auth_string
651
+ }
652
+ end
653
+
654
+ # Sign the request params using the secret
655
+ #
656
+ # @return [Hash]
657
+ def sign_params(params, secret)
658
+ text = params.values_at(
659
+ :keyName,
660
+ :ttl,
661
+ :capability,
662
+ :clientId,
663
+ :timestamp,
664
+ :nonce
665
+ ).map do |val|
666
+ "#{val}\n"
667
+ end.join('')
668
+
669
+ encode64(
670
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, text)
671
+ )
672
+ end
673
+
674
+ # Retrieve a token request from a specified URL, expects a JSON or text response
675
+ #
676
+ # @return [Hash]
677
+ def token_request_from_auth_url(auth_url, auth_options, token_params)
678
+ uri = URI.parse(auth_url)
679
+ connection = Faraday.new("#{uri.scheme}://#{uri.host}", connection_options)
680
+ method = auth_options[:auth_method] || options[:auth_method] || :get
681
+ params = (auth_options[:auth_params] || options[:auth_params] || {}).merge(token_params)
682
+
683
+ response = connection.public_send(method) do |request|
684
+ request.url uri.path
685
+ request.headers = auth_options[:auth_headers] || {}
686
+ if method.to_s.downcase == 'post'
687
+ request.body = params
688
+ else
689
+ request.params = (Addressable::URI.parse(uri.to_s).query_values || {}).merge(params)
690
+ end
691
+ end
692
+
693
+ if !response.body.kind_of?(Hash) && !response.headers['Content-Type'].to_s.match(%r{text/plain|application/jwt}i)
694
+ raise Ably::Exceptions::InvalidResponseBody,
695
+ "Content Type #{response.headers['Content-Type']} is not supported by this client library"
696
+ end
697
+
698
+ response.body
699
+ end
700
+
701
+ # Use the provided token to authenticate immediately and store the token details in +current_token_details+
702
+ def authorize_with_token(new_token_details)
703
+ if new_token_details && !new_token_details.from_token_string?
704
+ if !token_client_id_allowed?(new_token_details.client_id)
705
+ raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'")
706
+ end
707
+ configure_client_id new_token_details.client_id
708
+ end
709
+ configure_current_token_details new_token_details
710
+ end
711
+
712
+ # Returns a TokenDetails object if the provided token_details_obj argument is a TokenDetails object, Token String
713
+ # or TokenDetails JSON object.
714
+ # If the token_details_obj is not a Token or TokenDetails +nil+ is returned
715
+ def convert_to_token_details(token_details_obj)
716
+ case token_details_obj
717
+ when Ably::Models::TokenDetails
718
+ return token_details_obj
719
+ when Hash
720
+ return Ably::Models::TokenDetails.new(token_details_obj) if IdiomaticRubyWrapper(token_details_obj).has_key?(:issued)
721
+ when String
722
+ return Ably::Models::TokenDetails.new(token: token_details_obj)
723
+ end
724
+ end
725
+
726
+ # @return [Ably::Models::TokenDetails]
727
+ def send_token_request(token_request)
728
+ token_request = Ably::Models::TokenRequest(token_request)
729
+
730
+ response = client.post("/keys/#{token_request.key_name}/requestToken",
731
+ token_request.attributes, send_auth_header: false,
732
+ disable_automatic_reauthorize: true)
733
+
734
+ Ably::Models::TokenDetails.new(response.body)
735
+ end
736
+
737
+ # Return a Hash of connection options to initiate the Faraday::Connection with
738
+ #
739
+ # @return [Hash]
740
+ def connection_options
741
+ @connection_options ||= {
742
+ builder: middleware,
743
+ headers: {
744
+ accept: client.mime_type,
745
+ user_agent: user_agent
746
+ },
747
+ request: {
748
+ open_timeout: 5,
749
+ timeout: 10
750
+ }
751
+ }
752
+ end
753
+
754
+ # Return a Faraday middleware stack to initiate the Faraday::Connection with
755
+ #
756
+ # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
757
+ def middleware
758
+ @middleware ||= Faraday::RackBuilder.new do |builder|
759
+ setup_outgoing_middleware builder
760
+
761
+ # Raise exceptions if response code is invalid
762
+ builder.use Ably::Rest::Middleware::ExternalExceptions
763
+
764
+ setup_incoming_middleware builder, logger
765
+
766
+ # Set Faraday's HTTP adapter
767
+ builder.adapter Faraday.default_adapter
768
+ end
769
+ end
770
+
771
+ def auth_callback_present?
772
+ !!options[:auth_callback]
773
+ end
774
+
775
+ def token_url_present?
776
+ !!options[:auth_url]
777
+ end
778
+
779
+ def token_creatable_externally?
780
+ auth_callback_present? || token_url_present?
781
+ end
782
+
783
+ def api_key_present?
784
+ key_name && key_secret
785
+ end
786
+
787
+ def logger
788
+ client.logger
789
+ end
790
+ end
791
+ end