sockudo 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b7a967b624337aaad0668a3c7fba48c18ca82dbcbf61f48d2637ab6c468a8932
4
+ data.tar.gz: 6fca4167728cd368e7e0dc93fb3c2986604e2fc35c8fa762c7e2e2606fc33a08
5
+ SHA512:
6
+ metadata.gz: 1211b8badb256617a74201e39d6c7579a9351cf575a2fd68ee3bbdbada7b877ce6ddcb1ddfb34c37253bd276d8db470e9340f6150c75c653cbf70622aff78fa3
7
+ data.tar.gz: bbe0e470d9c7cd52119693f250139eeac6ace3b0b54f01a293aed1db9003e5260ff9f7681cf0783fcbd64d967b68e431b75ab8e4783c1c31904838f002d7c54c
data/CHANGELOG.md ADDED
@@ -0,0 +1,137 @@
1
+ # Changelog
2
+
3
+ ## 2.0.4
4
+
5
+ * [ADDED] Add `authenticate_user` method for user authentication flow
6
+
7
+ ## 2.0.3
8
+
9
+ * [FIXED] Corrected the channels limit when publishing events. Upped from 10 to 100.
10
+
11
+ ## 2.0.2
12
+
13
+ * [CHANGED] made encryption_master_key_base64 globally configurable
14
+
15
+ ## 2.0.1
16
+
17
+ * [CHANGED] Only include lib and essential docs in gem.
18
+
19
+ ## 2.0.0
20
+
21
+ * [CHANGED] Use TLS by default.
22
+ * [REMOVED] Support for Ruby 2.4 and 2.5.
23
+ * [FIXED] Handle empty or nil configuration.
24
+ * [REMOVED] Legacy Push Notification integration.
25
+ * [ADDED] Stalebot and Github actions.
26
+
27
+ ## 1.4.3
28
+
29
+ * [FIXED] Remove newline from end of base64 encoded strings, some decoders don't like
30
+ them.
31
+
32
+ ## 1.4.2
33
+ ==================
34
+
35
+ * [FIXED] Return `shared_secret` to support authenticating encrypted channels. Thanks
36
+ @Benjaminpjacobs
37
+
38
+ ## 1.4.1
39
+
40
+ * [CHANGED] Remove rbnacl from dependencies so we don't get errors when it isn't
41
+ required. Thanks @y-yagi!
42
+
43
+ ## 1.4.0
44
+
45
+ * [ADDED] Support for end-to-end encryption.
46
+
47
+ ## 1.3.3
48
+
49
+ * [CHANGED] Rewording to clarify "Pusher Channels" or simply "Channels" product name.
50
+
51
+ ## 1.3.2
52
+
53
+ * [FIXED] Return a specific error for "Request Entity Too Large" (body over 10KB).
54
+ * [ADDED] Add a `use_tls` option for SSL (defaults to false).
55
+ * [ADDED] Add a `from_url` client method (in addition to existing `from_env` option).
56
+ * [CHANGED] Improved documentation and fixed typos.
57
+ * [ADDED] Add Ruby 2.4 to test matrix.
58
+
59
+ ## 1.3.1
60
+
61
+ * [FIXED] Added missing client batch methods to default client delegations
62
+ * [CHANGED] Document raised exception in the `authenticate` method
63
+ * [FIXED] Fixes em-http-request from using v2.5.0 of `addressable` breaking builds.
64
+
65
+ ## 1.3.0
66
+
67
+ * [ADDED] Add support for sending push notifications on up to 10 interests.
68
+
69
+ ## 1.2.1
70
+
71
+ * [FIXED] Fixes Rails 5 compatibility. Use duck-typing to detect request object
72
+
73
+ ## 1.2.0
74
+
75
+ * [CHANGED] Minor release for Native notifications
76
+
77
+ ## 1.2.0.rc1
78
+
79
+ * [ADDED] Add support for Native notifications
80
+
81
+ ## 1.1.0
82
+
83
+ * [ADDED] Add support for batch events
84
+
85
+ ## 1.0.0
86
+
87
+ * [CHANGED] No breaking changes, this release is just to follow semver and show that we
88
+ are stable.
89
+
90
+ ## 0.18.0
91
+
92
+ * [ADDED] Introduce `Pusher::Client.from_env`
93
+ * [FIXED] Improve error handling on missing config
94
+
95
+ ## 0.17.0
96
+
97
+ * [ADDED] Introduce the `cluster` option.
98
+
99
+ ## 0.16.0
100
+
101
+ * [CHANGED] Bump httpclient version to 2.7
102
+ * [REMOVED] Ruby 1.8.7 is not supported anymore.
103
+
104
+ ## 0.15.2
105
+
106
+ * [CHANGED] Documented `Pusher.channel_info`, `Pusher.channels`
107
+ * [ADDED] Added `Pusher.channel_users`
108
+
109
+ ## 0.15.1
110
+
111
+ * [FIXED] Fixed a bug where the `authenticate` method added in 0.15.0 wasn't exposed on the Pusher class.
112
+
113
+ ## 0.15.0
114
+
115
+ * [ADDED] Added `Pusher.authenticate` method for authenticating private and presence channels.
116
+ This is prefered over the older `Pusher['a_channel'].authenticate(...)` style.
117
+
118
+ ## 0.14.6
119
+
120
+ * [CHANGED] Updated to use the `pusher-signature` gem instead of `signature`.
121
+ This resolves namespace related issues.
122
+
123
+ ## 0.14.5
124
+
125
+ * [SECURITY] Prevent auth delegation trough crafted socket IDs
126
+
127
+ ## 0.14.4
128
+
129
+ * [SECURITY] Prevent timing attack, update signature to v0.1.8
130
+ * [SECURITY] Prevent POODLE. Disable SSLv3, update httpclient to v2.5
131
+ * [FIXED] Fix channel name character limit.
132
+ * [ADDED] Adds support for listing users on a presence channel
133
+
134
+ ## 0.14.2
135
+
136
+ * [CHANGED] Bump httpclient to v2.4. See #62 (POODLE SSL)
137
+ * [CHANGED] Fix limited channel count at README.md. Thanks @tricknotes
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2013 Pusher
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # sockudo
2
+
3
+ Official Ruby server SDK for [Sockudo](https://github.com/sockudo/sockudo) — a fast, self-hosted WebSocket server with full Pusher HTTP API compatibility.
4
+
5
+ ## Supported Platforms
6
+
7
+ - **Ruby 3.0 or greater**
8
+ - Rails and other Ruby frameworks are supported provided you are running a supported Ruby version.
9
+
10
+ ## Installation
11
+
12
+ Add to your Gemfile:
13
+
14
+ ```ruby
15
+ gem 'sockudo'
16
+ ```
17
+
18
+ Then run `bundle install`, or install directly:
19
+
20
+ ```bash
21
+ gem install sockudo
22
+ ```
23
+
24
+ ## Getting Started
25
+
26
+ ```ruby
27
+ require 'sockudo'
28
+
29
+ sockudo = Sockudo::Client.new(
30
+ app_id: 'your-app-id',
31
+ key: 'your-app-key',
32
+ secret: 'your-app-secret',
33
+ host: '127.0.0.1',
34
+ port: 6001,
35
+ use_tls: false
36
+ )
37
+
38
+ sockudo.trigger('my-channel', 'my-event', { message: 'hello world' })
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ ### Instance Configuration
44
+
45
+ ```ruby
46
+ sockudo = Sockudo::Client.new(
47
+ app_id: 'your-app-id',
48
+ key: 'your-app-key',
49
+ secret: 'your-app-secret',
50
+ host: '127.0.0.1',
51
+ port: 6001,
52
+ use_tls: false
53
+ )
54
+ ```
55
+
56
+ `use_tls` is optional and defaults to `true`. It sets the scheme and port accordingly. A custom `port` value takes precedence over `use_tls`.
57
+
58
+ ### Global Configuration
59
+
60
+ ```ruby
61
+ Sockudo.app_id = 'your-app-id'
62
+ Sockudo.key = 'your-app-key'
63
+ Sockudo.secret = 'your-app-secret'
64
+ Sockudo.host = '127.0.0.1'
65
+ Sockudo.port = 6001
66
+ Sockudo.use_tls = false
67
+ ```
68
+
69
+ ### From Environment Variable
70
+
71
+ If `SOCKUDO_URL` is set in the environment, use `from_env` to configure automatically:
72
+
73
+ ```ruby
74
+ # Reads SOCKUDO_URL in the form: http://KEY:SECRET@HOST:PORT/apps/APP_ID
75
+ sockudo = Sockudo::Client.from_env
76
+ ```
77
+
78
+ Global configuration is also automatically read from `SOCKUDO_URL` when set.
79
+
80
+ ### HTTP Proxy
81
+
82
+ ```ruby
83
+ Sockudo.http_proxy = 'http://user:password@proxy-host:8080'
84
+ ```
85
+
86
+ ## Publishing Events
87
+
88
+ ### Single Channel
89
+
90
+ ```ruby
91
+ sockudo.trigger('my-channel', 'my-event', { message: 'hello world' })
92
+ ```
93
+
94
+ ### Multiple Channels
95
+
96
+ ```ruby
97
+ sockudo.trigger(['channel-1', 'channel-2'], 'my-event', { message: 'hello world' })
98
+ ```
99
+
100
+ Up to 10 channels per call.
101
+
102
+ ### Excluding a Socket Recipient
103
+
104
+ Pass a `socket_id` option to prevent the triggering connection from receiving its own event:
105
+
106
+ ```ruby
107
+ sockudo.trigger('my-channel', 'my-event', { message: 'hello' }, { socket_id: '123.456' })
108
+ ```
109
+
110
+ ### Batch Events
111
+
112
+ Send multiple events in a single HTTP request (up to 10 per call):
113
+
114
+ ```ruby
115
+ sockudo.trigger_batch([
116
+ { channel: 'channel-1', name: 'event-1', data: { x: 1 } },
117
+ { channel: 'channel-2', name: 'event-2', data: { x: 2 } },
118
+ { channel: 'channel-3', name: 'event-3', data: { x: 3 } },
119
+ ])
120
+ ```
121
+
122
+ ## Idempotent Publishing
123
+
124
+ Pass an `idempotency_key` to safely retry publishes without causing duplicate deliveries:
125
+
126
+ ```ruby
127
+ sockudo.trigger(
128
+ 'my-channel',
129
+ 'my-event',
130
+ { message: 'hello' },
131
+ { idempotency_key: 'order-shipped-order-789' }
132
+ )
133
+ ```
134
+
135
+ The server deduplicates publishes with the same key within the configured window.
136
+
137
+ ## Channel Authorization
138
+
139
+ ### Private Channel
140
+
141
+ ```ruby
142
+ auth = sockudo.authenticate('private-my-channel', params[:socket_id])
143
+ # Returns JSON: {"auth":"key:signature"}
144
+ ```
145
+
146
+ ### Presence Channel
147
+
148
+ ```ruby
149
+ auth = sockudo.authenticate(
150
+ 'presence-my-channel',
151
+ params[:socket_id],
152
+ user_id: 'user-123',
153
+ user_info: { name: 'Jane Doe', role: 'admin' }
154
+ )
155
+ # Returns JSON: {"auth":"key:signature","channel_data":"..."}
156
+ ```
157
+
158
+ ## User Authentication
159
+
160
+ ```ruby
161
+ auth = sockudo.authenticate_user(params[:socket_id], { id: 'user-123', name: 'Jane Doe' })
162
+ ```
163
+
164
+ ## Webhooks
165
+
166
+ Create a webhook object from a `Rack::Request` (available as `request` in Rails controllers and Sinatra handlers):
167
+
168
+ ```ruby
169
+ webhook = sockudo.webhook(request)
170
+
171
+ if webhook.valid?
172
+ webhook.events.each do |event|
173
+ case event['name']
174
+ when 'channel_occupied'
175
+ puts "Channel occupied: #{event['channel']}"
176
+ when 'channel_vacated'
177
+ puts "Channel vacated: #{event['channel']}"
178
+ when 'client_event'
179
+ puts "Client event: #{event['event']} on #{event['channel']}"
180
+ end
181
+ end
182
+
183
+ render plain: 'ok'
184
+ else
185
+ render plain: 'invalid', status: 401
186
+ end
187
+ ```
188
+
189
+ ## Application State
190
+
191
+ ```ruby
192
+ # Info about a channel
193
+ info = sockudo.channel_info('my-channel')
194
+ occupied = info[:occupied]
195
+
196
+ # User count for a presence channel
197
+ info = sockudo.channel_info('presence-my-channel', info: 'user_count')
198
+ user_count = info[:user_count]
199
+
200
+ # List channels (optionally filtered)
201
+ result = sockudo.channels(filter_by_prefix: 'presence-')
202
+
203
+ # Users in a presence channel
204
+ result = sockudo.channel_users('presence-my-channel')
205
+ ```
206
+
207
+ ## Async Requests
208
+
209
+ There are two reasons to use async methods: avoiding blocking in a request-response cycle, or running inside an event loop.
210
+
211
+ ### With EventMachine
212
+
213
+ Add `em-http-request` to your Gemfile and run with the EventMachine reactor active (e.g. inside Thin):
214
+
215
+ ```ruby
216
+ sockudo.get_async('/channels').callback { |response|
217
+ puts response[:channels].inspect
218
+ }.errback { |error|
219
+ puts "Error: #{error}"
220
+ }
221
+
222
+ sockudo.trigger_async('my-channel', 'my-event', { message: 'hello' }).callback { |response|
223
+ puts 'Triggered'
224
+ }
225
+ ```
226
+
227
+ ### Without EventMachine (Threaded)
228
+
229
+ If the EventMachine reactor is not running, async requests are made using threads managed by `httpclient`. An `HTTPClient::Connection` object is returned immediately.
230
+
231
+ ```ruby
232
+ sockudo.trigger_async('my-channel', 'my-event', { message: 'hello' })
233
+ ```
234
+
235
+ ## Error Handling
236
+
237
+ All errors are descendants of `Sockudo::Error`:
238
+
239
+ ```ruby
240
+ begin
241
+ sockudo.trigger('my-channel', 'my-event', { message: 'hello' })
242
+ rescue Sockudo::AuthenticationError => e
243
+ # invalid credentials
244
+ rescue Sockudo::HTTPError => e
245
+ # network or HTTP-level error
246
+ rescue Sockudo::Error => e
247
+ # catch-all
248
+ end
249
+ ```
250
+
251
+ ## Logging
252
+
253
+ Assign any logger compatible with Ruby's standard `Logger` interface:
254
+
255
+ ```ruby
256
+ # Rails
257
+ Sockudo.logger = Rails.logger
258
+
259
+ # Default: logs at INFO level to STDOUT
260
+ ```
261
+
262
+ ## Testing
263
+
264
+ ```bash
265
+ bundle install
266
+ bundle exec rake spec
267
+ ```
268
+
269
+ ## Pusher SDK Compatibility
270
+
271
+ Sockudo implements the full Pusher HTTP API. If you prefer to use the official `pusher` gem or are migrating from Pusher, point it at your Sockudo instance:
272
+
273
+ ```ruby
274
+ require 'pusher'
275
+
276
+ pusher = Pusher::Client.new(
277
+ app_id: 'your-app-id',
278
+ key: 'your-app-key',
279
+ secret: 'your-app-secret',
280
+ host: '127.0.0.1',
281
+ port: 6001
282
+ )
283
+ ```
284
+
285
+ All standard Pusher SDK calls work against a self-hosted Sockudo server without modification.
286
+
287
+ ## License
288
+
289
+ MIT
@@ -0,0 +1,179 @@
1
+ require 'openssl'
2
+ require 'multi_json'
3
+
4
+ module Sockudo
5
+ # Delegates operations for a specific channel from a client
6
+ class Channel
7
+ attr_reader :name
8
+ INVALID_CHANNEL_REGEX = /[^A-Za-z0-9_\-=@,.;]/
9
+
10
+ def initialize(_, name, client = Sockudo)
11
+ if Sockudo::Channel::INVALID_CHANNEL_REGEX.match(name)
12
+ raise Sockudo::Error, "Illegal channel name '#{name}'"
13
+ elsif name.length > 200
14
+ raise Sockudo::Error, "Channel name too long (limit 164 characters) '#{name}'"
15
+ end
16
+ @name = name
17
+ @client = client
18
+ end
19
+
20
+ # Trigger event asynchronously using EventMachine::HttpRequest
21
+ #
22
+ # [Deprecated] This method will be removed in a future gem version. Please
23
+ # switch to Sockudo.trigger_async or Sockudo::Client#trigger_async instead
24
+ #
25
+ # @param (see #trigger!)
26
+ # @return [EM::DefaultDeferrable]
27
+ # Attach a callback to be notified of success (with no parameters).
28
+ # Attach an errback to be notified of failure (with an error parameter
29
+ # which includes the HTTP status code returned)
30
+ # @raise [LoadError] unless em-http-request gem is available
31
+ # @raise [Sockudo::Error] unless the eventmachine reactor is running. You
32
+ # probably want to run your application inside a server such as thin
33
+ #
34
+ def trigger_async(event_name, data, socket_id = nil)
35
+ params = {}
36
+ if socket_id
37
+ validate_socket_id(socket_id)
38
+ params[:socket_id] = socket_id
39
+ end
40
+ @client.trigger_async(name, event_name, data, params)
41
+ end
42
+
43
+ # Trigger event
44
+ #
45
+ # [Deprecated] This method will be removed in a future gem version. Please
46
+ # switch to Sockudo.trigger or Sockudo::Client#trigger instead
47
+ #
48
+ # @example
49
+ # begin
50
+ # Sockudo['my-channel'].trigger!('an_event', {:some => 'data'})
51
+ # rescue Sockudo::Error => e
52
+ # # Do something on error
53
+ # end
54
+ #
55
+ # @param data [Object] Event data to be triggered in javascript.
56
+ # Objects other than strings will be converted to JSON
57
+ # @param socket_id Allows excluding a given socket_id from receiving the
58
+ # event - see http://sockudo.com/docs/publisher_api_guide/publisher_excluding_recipients for more info
59
+ #
60
+ # @raise [Sockudo::Error] on invalid Sockudo response - see the error message for more details
61
+ # @raise [Sockudo::HTTPError] on any error raised inside http client - the original error is available in the original_error attribute
62
+ #
63
+ def trigger!(event_name, data, socket_id = nil)
64
+ params = {}
65
+ if socket_id
66
+ validate_socket_id(socket_id)
67
+ params[:socket_id] = socket_id
68
+ end
69
+ @client.trigger(name, event_name, data, params)
70
+ end
71
+
72
+ # Trigger event, catching and logging any errors.
73
+ #
74
+ # [Deprecated] This method will be removed in a future gem version. Please
75
+ # switch to Sockudo.trigger or Sockudo::Client#trigger instead
76
+ #
77
+ # @note CAUTION! No exceptions will be raised on failure
78
+ # @param (see #trigger!)
79
+ #
80
+ def trigger(event_name, data, socket_id = nil)
81
+ trigger!(event_name, data, socket_id)
82
+ rescue Sockudo::Error => e
83
+ Sockudo.logger.error("#{e.message} (#{e.class})")
84
+ Sockudo.logger.debug(e.backtrace.join("\n"))
85
+ end
86
+
87
+ # Request info for a channel
88
+ #
89
+ # @example Response
90
+ # [{:occupied=>true, :subscription_count => 12}]
91
+ #
92
+ # @param info [Array] Array of attributes required (as lowercase strings)
93
+ # @return [Hash] Hash of requested attributes for this channel
94
+ # @raise [Sockudo::Error] on invalid Sockudo response - see the error message for more details
95
+ # @raise [Sockudo::HTTPError] on any error raised inside http client - the original error is available in the original_error attribute
96
+ #
97
+ def info(attributes = [])
98
+ @client.channel_info(name, :info => attributes.join(','))
99
+ end
100
+
101
+ # Request users for a presence channel
102
+ # Only works on presence channels (see: http://sockudo.com/docs/client_api_guide/client_presence_channels and https://sockudo.com/docs/rest_api)
103
+ #
104
+ # @example Response
105
+ # [{:id=>"4"}]
106
+ #
107
+ # @param params [Hash] Hash of parameters for the API - see REST API docs
108
+ # @return [Hash] Array of user hashes for this channel
109
+ # @raise [Sockudo::Error] on invalid Sockudo response - see the error message for more details
110
+ # @raise [Sockudo::HTTPError] on any error raised inside Net::HTTP - the original error is available in the original_error attribute
111
+ #
112
+ def users(params = {})
113
+ @client.channel_users(name, params)[:users]
114
+ end
115
+
116
+ # Compute authentication string required as part of the authentication
117
+ # endpoint response. Generally the authenticate method should be used in
118
+ # preference to this one
119
+ #
120
+ # @param socket_id [String] Each Sockudo socket connection receives a
121
+ # unique socket_id. This is sent from sockudo.js to your server when
122
+ # channel authentication is required.
123
+ # @param custom_string [String] Allows signing additional data
124
+ # @return [String]
125
+ #
126
+ # @raise [Sockudo::Error] if socket_id or custom_string invalid
127
+ #
128
+ def authentication_string(socket_id, custom_string = nil)
129
+ string_to_sign = [socket_id, name, custom_string].compact.map(&:to_s).join(':')
130
+
131
+ _authentication_string(socket_id, string_to_sign, @client.authentication_token, custom_string)
132
+ end
133
+
134
+ # Generate the expected response for an authentication endpoint.
135
+ # See http://sockudo.com/docs/authenticating_users for details.
136
+ #
137
+ # @example Private channels
138
+ # render :json => Sockudo['private-my_channel'].authenticate(params[:socket_id])
139
+ #
140
+ # @example Presence channels
141
+ # render :json => Sockudo['presence-my_channel'].authenticate(params[:socket_id], {
142
+ # :user_id => current_user.id, # => required
143
+ # :user_info => { # => optional - for example
144
+ # :name => current_user.name,
145
+ # :email => current_user.email
146
+ # }
147
+ # })
148
+ #
149
+ # @param socket_id [String]
150
+ # @param custom_data [Hash] used for example by private channels
151
+ #
152
+ # @return [Hash]
153
+ #
154
+ # @raise [Sockudo::Error] if socket_id or custom_data is invalid
155
+ #
156
+ # @private Custom data is sent to server as JSON-encoded string
157
+ #
158
+ def authenticate(socket_id, custom_data = nil)
159
+ custom_data = MultiJson.encode(custom_data) if custom_data
160
+ auth = authentication_string(socket_id, custom_data)
161
+ r = {:auth => auth}
162
+ r[:channel_data] = custom_data if custom_data
163
+ r
164
+ end
165
+
166
+ def shared_secret(encryption_master_key)
167
+ return unless encryption_master_key
168
+
169
+ secret_string = @name + encryption_master_key
170
+ digest = OpenSSL::Digest::SHA256.new
171
+ digest << secret_string
172
+ digest.digest
173
+ end
174
+
175
+ private
176
+
177
+ include Sockudo::Utils
178
+ end
179
+ end