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
@@ -0,0 +1,144 @@
1
+ require 'ably/modules/state_machine'
2
+
3
+ module Ably::Realtime
4
+ class Connection
5
+ # Internal class to manage connection state, recovery and state transitions for {Ably::Realtime::Connection}
6
+ class ConnectionStateMachine
7
+ include Ably::Modules::StateMachine
8
+
9
+ # States supported by this StateMachine match #{Connection::STATE}s
10
+ # :initialized
11
+ # :connecting
12
+ # :connected
13
+ # :disconnected
14
+ # :suspended
15
+ # :closing
16
+ # :closed
17
+ # :failed
18
+ Connection::STATE.each_with_index do |state_enum, index|
19
+ state state_enum.to_sym, initial: index == 0
20
+ end
21
+
22
+ transition :from => :initialized, :to => [:connecting, :closing]
23
+ transition :from => :connecting, :to => [:connected, :failed, :closing, :disconnected, :suspended]
24
+ transition :from => :connected, :to => [:disconnected, :suspended, :closing, :failed]
25
+ transition :from => :disconnected, :to => [:connecting, :closing, :suspended, :failed]
26
+ transition :from => :suspended, :to => [:connecting, :closing, :failed]
27
+ transition :from => :closing, :to => [:closed]
28
+ transition :from => :closed, :to => [:connecting]
29
+ transition :from => :failed, :to => [:connecting]
30
+
31
+ after_transition do |connection, transition|
32
+ connection.synchronize_state_with_statemachine
33
+ end
34
+
35
+ after_transition(to: [:connecting], from: [:initialized, :closed, :failed]) do |connection|
36
+ connection.manager.setup_transport
37
+ end
38
+
39
+ after_transition(to: [:connecting], from: [:failed]) do |connection|
40
+ connection.manager.reintialize_failed_chanels
41
+ end
42
+
43
+ after_transition(to: [:connecting], from: [:disconnected, :suspended]) do |connection|
44
+ connection.manager.reconnect_transport
45
+ end
46
+
47
+ before_transition(to: [:connected]) do |connection, current_transition|
48
+ connection.manager.connected current_transition.metadata.protocol_message
49
+ end
50
+
51
+ after_transition(to: [:connected]) do |connection, current_transition|
52
+ error = current_transition.metadata.reason
53
+ if is_error_type?(error)
54
+ connection.logger.warn { "ConnectionManager: Connected with error - #{error.message}" }
55
+ end
56
+ end
57
+
58
+ after_transition(to: [:disconnected, :suspended], from: [:connecting]) do |connection, current_transition|
59
+ err = error_from_state_change(current_transition)
60
+ connection.manager.respond_to_transport_disconnected_when_connecting err
61
+ end
62
+
63
+ after_transition(to: [:disconnected, :suspended], from: [:connected]) do |connection, current_transition|
64
+ err = error_from_state_change(current_transition)
65
+ connection.manager.respond_to_transport_disconnected_whilst_connected err
66
+ end
67
+
68
+ after_transition(to: [:suspended]) do |connection, current_transition|
69
+ err = error_from_state_change(current_transition)
70
+ connection.manager.suspend_active_channels err
71
+ end
72
+
73
+ after_transition(to: [:disconnected, :suspended]) do |connection|
74
+ connection.manager.destroy_transport # never reuse a transport if the connection has failed
75
+ end
76
+
77
+ before_transition(to: [:failed]) do |connection, current_transition|
78
+ err = error_from_state_change(current_transition)
79
+ connection.manager.fail err
80
+ end
81
+
82
+ after_transition(to: [:failed]) do |connection, current_transition|
83
+ err = error_from_state_change(current_transition)
84
+ connection.manager.fail_active_channels err
85
+ end
86
+
87
+ # RTN7C - If a connection enters the SUSPENDED, CLOSED or FAILED state...
88
+ # the client should consider the delivery of those messages as failed
89
+ after_transition(to: [:suspended, :closed, :failed]) do |connection, current_transition|
90
+ err = error_from_state_change(current_transition)
91
+ connection.manager.nack_messages_on_all_channels err
92
+ end
93
+
94
+ after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
95
+ connection.manager.force_close_connection
96
+ end
97
+
98
+ after_transition(to: [:closing], from: [:connecting, :connected]) do |connection|
99
+ connection.manager.close_connection
100
+ end
101
+
102
+ before_transition(to: [:closed], from: [:closing]) do |connection|
103
+ connection.manager.destroy_transport
104
+ end
105
+
106
+ after_transition(to: [:closed]) do |connection|
107
+ connection.manager.detach_active_channels
108
+ end
109
+
110
+ # Transitions responsible for updating connection#error_reason
111
+ before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
112
+ err = error_from_state_change(current_transition)
113
+ connection.set_failed_connection_error_reason err
114
+ end
115
+
116
+ before_transition(to: [:connected, :closed]) do |connection, current_transition|
117
+ err = error_from_state_change(current_transition)
118
+ if err
119
+ connection.set_failed_connection_error_reason err
120
+ else
121
+ # Connected & Closed are "healthy" final states so reset the error reason
122
+ connection.clear_error_reason
123
+ end
124
+ end
125
+
126
+ def self.error_from_state_change(current_transition)
127
+ # ConnectionStateChange object is always passed in current_transition metadata object
128
+ connection_state_change = current_transition.metadata
129
+ # Reason attribute contains errors
130
+ err = connection_state_change && connection_state_change.reason
131
+ err if is_error_type?(err)
132
+ end
133
+
134
+ private
135
+ def connection
136
+ object
137
+ end
138
+
139
+ def logger
140
+ connection.logger
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,287 @@
1
+ require 'openssl'
2
+
3
+ module Ably::Realtime
4
+ class Connection
5
+ # EventMachine WebSocket transport
6
+ # @api private
7
+ class WebsocketTransport < EventMachine::Connection
8
+ include Ably::Modules::EventEmitter
9
+ extend Ably::Modules::Enum
10
+
11
+ # Valid WebSocket connection states
12
+ STATE = ruby_enum('STATE',
13
+ :initialized,
14
+ :connecting,
15
+ :connected,
16
+ :disconnecting,
17
+ :disconnected
18
+ )
19
+ include Ably::Modules::StateEmitter
20
+
21
+ attr_reader :host
22
+
23
+ def initialize(connection, url)
24
+ @connection = connection
25
+ @state = STATE.Initialized
26
+ @url = url
27
+ @host = URI.parse(url).hostname
28
+
29
+ setup_event_handlers
30
+ end
31
+
32
+ # Disconnect the socket transport connection and write all pending text.
33
+ # If Disconnected state is not automatically emitted, it will be emitted automatically
34
+ #
35
+ # @return [void]
36
+ #
37
+ # @api public
38
+ def disconnect
39
+ close_connection_after_writing
40
+ change_state STATE.Disconnecting
41
+ create_timer(2) do
42
+ # if connection is not disconnected within 2s, set state as disconnected
43
+ change_state STATE.Disconnected unless disconnected?
44
+ end
45
+ end
46
+
47
+ # Network connection has been established
48
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
49
+ def post_init
50
+ clear_timer
51
+ change_state STATE.Connecting
52
+ setup_driver
53
+ end
54
+
55
+ # Remote TCP connection attempt completes successfully
56
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
57
+ def connection_completed
58
+ change_state STATE.Connected
59
+ start_tls(tls_opts) if client.use_tls?
60
+ driver.start
61
+ end
62
+
63
+ # Called by the event loop whenever data has been received by the network connection.
64
+ # Simply pass onto the WebSocket driver to process and determine content boundaries.
65
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
66
+ def receive_data(data)
67
+ driver.parse(data)
68
+ end
69
+
70
+ # Called whenever a connection (either a server or client connection) is closed
71
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
72
+ def unbind
73
+ change_state STATE.Disconnected, reason_closed || 'Websocket connection closed unexpectedly'
74
+ end
75
+
76
+ # URL end point including initialization configuration
77
+ # {http://www.rubydoc.info/gems/websocket-driver/0.3.5/WebSocket/Driver WebSocket::Driver} interface
78
+ def url
79
+ @url
80
+ end
81
+
82
+ # {http://www.rubydoc.info/gems/websocket-driver/0.3.5/WebSocket/Driver WebSocket::Driver} interface
83
+ def write(data)
84
+ send_data(data)
85
+ end
86
+
87
+ # TLS verification support, original implementation by Mislav Marohnić:
88
+ #
89
+ # https://github.com/lostisland/faraday/commit/63cf47c95b573539f047c729bd9ad67560bc83ff
90
+ def ssl_verify_peer(cert_string)
91
+ cert = nil
92
+ begin
93
+ cert = OpenSSL::X509::Certificate.new(cert_string)
94
+ rescue OpenSSL::X509::CertificateError => e
95
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate: #{e.message}"
96
+ return false
97
+ end
98
+
99
+ @last_seen_cert = cert
100
+
101
+ if certificate_store.verify(@last_seen_cert)
102
+ begin
103
+ certificate_store.add_cert(@last_seen_cert)
104
+ rescue OpenSSL::X509::StoreError => e
105
+ unless e.message == 'cert already in hash table'
106
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate: #{e.message}"
107
+ return false
108
+ end
109
+ end
110
+ true
111
+ else
112
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate"
113
+ false
114
+ end
115
+ end
116
+
117
+ def ssl_handshake_completed
118
+ unless OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, host)
119
+ disconnect_with_reason "Websocket host '#{host}' returned an invalid TLS certificate"
120
+ false
121
+ else
122
+ true
123
+ end
124
+ end
125
+
126
+ def certificate_store
127
+ @certificate_store ||= OpenSSL::X509::Store.new.tap do |store|
128
+ store.set_default_paths
129
+ end
130
+ end
131
+
132
+ # True if socket connection is ready to be released
133
+ # i.e. it is not currently connecting or connected
134
+ def ready_for_release?
135
+ !connecting? && !connected?
136
+ end
137
+
138
+ # @return [Ably::Util::PubSub] Websocket Transport internal incoming protocol message bus
139
+ #
140
+ # @api private
141
+ def __incoming_protocol_msgbus__
142
+ @__incoming_protocol_msgbus__ ||= create_pub_sub_message_bus
143
+ end
144
+
145
+ # @return [Ably::Util::PubSub] Websocket Transport internal outgoing protocol message bus
146
+ #
147
+ # @api private
148
+ def __outgoing_protocol_msgbus__
149
+ @__outgoing_protocol_msgbus__ ||= create_pub_sub_message_bus
150
+ end
151
+
152
+ private
153
+ def driver
154
+ @driver
155
+ end
156
+
157
+ def connection
158
+ @connection
159
+ end
160
+
161
+ def disconnect_with_reason(reason)
162
+ client.logger.error { "WebsocketTransport: Disconnecting due to error: #{reason}" }
163
+ @reason_closed = reason
164
+ disconnect
165
+ end
166
+
167
+ def reason_closed
168
+ @reason_closed
169
+ end
170
+
171
+ # Send object down the WebSocket driver connection as a serialized string/byte array based on protocol
172
+ # @param [Object] object to serialize and send to the WebSocket driver
173
+ def send_object(object)
174
+ case client.protocol
175
+ when :json
176
+ driver.text(object.to_json)
177
+ when :msgpack
178
+ driver.binary(object.to_msgpack.unpack('C*'))
179
+ else
180
+ client.logger.fatal { "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket" }
181
+ end
182
+ end
183
+
184
+ def setup_event_handlers
185
+ __outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
186
+ send_object protocol_message
187
+ client.logger.debug { "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}" }
188
+ end
189
+ end
190
+
191
+ def clear_timer
192
+ if defined?(@timer) && @timer
193
+ @timer.cancel
194
+ @timer = nil
195
+ end
196
+ end
197
+
198
+ def create_timer(period)
199
+ @timer = EventMachine::Timer.new(period) do
200
+ yield
201
+ end
202
+ end
203
+
204
+ def setup_driver
205
+ @driver = WebSocket::Driver.client(self)
206
+
207
+ driver.on("open") do
208
+ logger.debug { "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message" }
209
+ end
210
+
211
+ driver.on("message") do |event|
212
+ event_data = parse_event_data(event.data).freeze
213
+ protocol_message = Ably::Models::ProtocolMessage.new(event_data, logger: logger)
214
+ action_name = Ably::Models::ProtocolMessage::ACTION[event_data['action']] rescue event_data['action']
215
+ logger.debug { "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}" }
216
+
217
+ if protocol_message.invalid?
218
+ error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nConnection moving to the failed state as the protocol is invalid and unsupported", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
219
+ logger.fatal { "WebsocketTransport: #{error.message}" }
220
+ failed_protocol_message = Ably::Models::ProtocolMessage.new(
221
+ action: Ably::Models::ProtocolMessage::ACTION.Error,
222
+ error: error.as_json,
223
+ logger: logger
224
+ )
225
+ __incoming_protocol_msgbus__.publish :protocol_message, failed_protocol_message
226
+ else
227
+ __incoming_protocol_msgbus__.publish :protocol_message, protocol_message
228
+ end
229
+ end
230
+
231
+ driver.on("ping") do
232
+ __incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Heartbeat, source: :websocket)
233
+ end
234
+
235
+ driver.on("error") do |error|
236
+ logger.error { "WebsocketTransport: Protocol Error on transports - #{error.message}" }
237
+ end
238
+
239
+ @reason_closed = nil
240
+ driver.on("closed") do |event|
241
+ @reason_closed = "#{event.code}: #{event.reason}"
242
+ logger.warn { "WebsocketTransport: Driver reported transport as closed - #{reason_closed}" }
243
+ end
244
+ end
245
+
246
+ def client
247
+ connection.client
248
+ end
249
+
250
+ # Used to log transport messages
251
+ def logger
252
+ connection.logger
253
+ end
254
+
255
+ def parse_event_data(data)
256
+ case client.protocol
257
+ when :json
258
+ JSON.parse(data)
259
+ when :msgpack
260
+ MessagePack.unpack(data)
261
+ else
262
+ client.logger.fatal { "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}" }
263
+ data
264
+ end
265
+ end
266
+
267
+ def create_pub_sub_message_bus
268
+ Ably::Util::PubSub.new(
269
+ coerce_into: lambda do |event|
270
+ raise KeyError, "Expected :protocol_message, :#{event} is disallowed" unless event == :protocol_message
271
+ :protocol_message
272
+ end
273
+ )
274
+ end
275
+
276
+ # TLS options to pass to EventMachine::Connection#start_tls
277
+ #
278
+ # See https://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection#start_tls-instance_method
279
+ def tls_opts
280
+ {
281
+ sni_hostname: host,
282
+ verify_peer: true,
283
+ }
284
+ end
285
+ end
286
+ end
287
+ end