pusher 0.17.0 → 2.0.3
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 +5 -5
- data/CHANGELOG.md +112 -38
- data/README.md +134 -64
- data/lib/pusher/channel.rb +26 -10
- data/lib/pusher/client.rb +170 -63
- data/lib/pusher/request.rb +6 -2
- data/lib/pusher/version.rb +3 -0
- data/lib/pusher/webhook.rb +2 -2
- data/lib/pusher.rb +18 -10
- metadata +59 -43
- data/.document +0 -5
- data/.gemtest +0 -0
- data/.gitignore +0 -24
- data/.travis.yml +0 -19
- data/Gemfile +0 -2
- data/Rakefile +0 -11
- data/examples/async_message.rb +0 -28
- data/pusher.gemspec +0 -31
- data/spec/channel_spec.rb +0 -168
- data/spec/client_spec.rb +0 -488
- data/spec/spec_helper.rb +0 -26
- data/spec/web_hook_spec.rb +0 -117
data/lib/pusher/client.rb
CHANGED
@@ -1,55 +1,80 @@
|
|
1
|
+
require 'base64'
|
1
2
|
require 'pusher-signature'
|
2
3
|
|
3
4
|
module Pusher
|
4
5
|
class Client
|
5
|
-
attr_accessor :scheme, :host, :port, :app_id, :key, :secret
|
6
|
+
attr_accessor :scheme, :host, :port, :app_id, :key, :secret, :encryption_master_key
|
6
7
|
attr_reader :http_proxy, :proxy
|
7
8
|
attr_writer :connect_timeout, :send_timeout, :receive_timeout,
|
8
9
|
:keep_alive_timeout
|
9
10
|
|
10
11
|
## CONFIGURATION ##
|
12
|
+
DEFAULT_CONNECT_TIMEOUT = 5
|
13
|
+
DEFAULT_SEND_TIMEOUT = 5
|
14
|
+
DEFAULT_RECEIVE_TIMEOUT = 5
|
15
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT = 30
|
16
|
+
DEFAULT_CLUSTER = "mt1"
|
17
|
+
|
18
|
+
# Loads the configuration from an url in the environment
|
19
|
+
def self.from_env(key = 'PUSHER_URL')
|
20
|
+
url = ENV[key] || raise(ConfigurationError, key)
|
21
|
+
from_url(url)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Loads the configuration from a url
|
25
|
+
def self.from_url(url)
|
26
|
+
client = new
|
27
|
+
client.url = url
|
28
|
+
client
|
29
|
+
end
|
11
30
|
|
12
31
|
def initialize(options = {})
|
13
|
-
|
14
|
-
|
15
|
-
:port => 80,
|
16
|
-
}
|
17
|
-
merged_options = default_options.merge(options)
|
32
|
+
@scheme = "https"
|
33
|
+
@port = options[:port] || 443
|
18
34
|
|
19
|
-
if options.
|
20
|
-
|
21
|
-
elsif options.has_key?(:cluster)
|
22
|
-
merged_options[:host] = "api-#{options[:cluster]}.pusher.com"
|
23
|
-
else
|
24
|
-
merged_options[:host] = "api.pusherapp.com"
|
35
|
+
if options.key?(:encrypted)
|
36
|
+
warn "[DEPRECATION] `encrypted` is deprecated and will be removed in the next major version. Use `use_tls` instead."
|
25
37
|
end
|
26
38
|
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
if options[:use_tls] == false || options[:encrypted] == false
|
40
|
+
@scheme = "http"
|
41
|
+
@port = options[:port] || 80
|
42
|
+
end
|
30
43
|
|
31
|
-
@
|
32
|
-
|
44
|
+
@app_id = options[:app_id]
|
45
|
+
@key = options[:key]
|
46
|
+
@secret = options[:secret]
|
47
|
+
|
48
|
+
@host = options[:host]
|
49
|
+
@host ||= "api-#{options[:cluster]}.pusher.com" unless options[:cluster].nil? || options[:cluster].empty?
|
50
|
+
@host ||= "api-#{DEFAULT_CLUSTER}.pusher.com"
|
51
|
+
|
52
|
+
@encryption_master_key = Base64.strict_decode64(options[:encryption_master_key_base64]) if options[:encryption_master_key_base64]
|
53
|
+
|
54
|
+
@http_proxy = options[:http_proxy]
|
33
55
|
|
34
56
|
# Default timeouts
|
35
|
-
@connect_timeout =
|
36
|
-
@send_timeout =
|
37
|
-
@receive_timeout =
|
38
|
-
@keep_alive_timeout =
|
57
|
+
@connect_timeout = DEFAULT_CONNECT_TIMEOUT
|
58
|
+
@send_timeout = DEFAULT_SEND_TIMEOUT
|
59
|
+
@receive_timeout = DEFAULT_RECEIVE_TIMEOUT
|
60
|
+
@keep_alive_timeout = DEFAULT_KEEP_ALIVE_TIMEOUT
|
39
61
|
end
|
40
62
|
|
41
63
|
# @private Returns the authentication token for the client
|
42
64
|
def authentication_token
|
65
|
+
raise ConfigurationError, :key unless @key
|
66
|
+
raise ConfigurationError, :secret unless @secret
|
43
67
|
Pusher::Signature::Token.new(@key, @secret)
|
44
68
|
end
|
45
69
|
|
46
70
|
# @private Builds a url for this app, optionally appending a path
|
47
71
|
def url(path = nil)
|
72
|
+
raise ConfigurationError, :app_id unless @app_id
|
48
73
|
URI::Generic.build({
|
49
|
-
:
|
50
|
-
:
|
51
|
-
:
|
52
|
-
:
|
74
|
+
scheme: @scheme,
|
75
|
+
host: @host,
|
76
|
+
port: @port,
|
77
|
+
path: "/apps/#{@app_id}#{path}"
|
53
78
|
})
|
54
79
|
end
|
55
80
|
|
@@ -73,13 +98,12 @@ module Pusher
|
|
73
98
|
@http_proxy = http_proxy
|
74
99
|
uri = URI.parse(http_proxy)
|
75
100
|
@proxy = {
|
76
|
-
:
|
77
|
-
:
|
78
|
-
:
|
79
|
-
:
|
80
|
-
:
|
101
|
+
scheme: uri.scheme,
|
102
|
+
host: uri.host,
|
103
|
+
port: uri.port,
|
104
|
+
user: uri.user,
|
105
|
+
password: uri.password
|
81
106
|
}
|
82
|
-
@http_proxy
|
83
107
|
end
|
84
108
|
|
85
109
|
# Configure whether Pusher API calls should be made over SSL
|
@@ -99,6 +123,8 @@ module Pusher
|
|
99
123
|
end
|
100
124
|
|
101
125
|
def cluster=(cluster)
|
126
|
+
cluster = DEFAULT_CLUSTER if cluster.nil? || cluster.empty?
|
127
|
+
|
102
128
|
@host = "api-#{cluster}.pusher.com"
|
103
129
|
end
|
104
130
|
|
@@ -108,7 +134,13 @@ module Pusher
|
|
108
134
|
@connect_timeout, @send_timeout, @receive_timeout = value, value, value
|
109
135
|
end
|
110
136
|
|
111
|
-
|
137
|
+
# Set an encryption_master_key to use with private-encrypted channels from
|
138
|
+
# a base64 encoded string.
|
139
|
+
def encryption_master_key_base64=(s)
|
140
|
+
@encryption_master_key = s ? Base64.strict_decode64(s) : nil
|
141
|
+
end
|
142
|
+
|
143
|
+
## INTERACT WITH THE API ##
|
112
144
|
|
113
145
|
def resource(path)
|
114
146
|
Resource.new(self, path)
|
@@ -133,7 +165,7 @@ module Pusher
|
|
133
165
|
# @raise [Pusher::HTTPError] Error raised inside http client. The original error is wrapped in error.original_error
|
134
166
|
#
|
135
167
|
def get(path, params = {})
|
136
|
-
|
168
|
+
resource(path).get(params)
|
137
169
|
end
|
138
170
|
|
139
171
|
# GET arbitrary REST API resource using an asynchronous http client.
|
@@ -149,20 +181,20 @@ module Pusher
|
|
149
181
|
# @return Either an EM::DefaultDeferrable or a HTTPClient::Connection
|
150
182
|
#
|
151
183
|
def get_async(path, params = {})
|
152
|
-
|
184
|
+
resource(path).get_async(params)
|
153
185
|
end
|
154
186
|
|
155
187
|
# POST arbitrary REST API resource using a synchronous http client.
|
156
188
|
# Works identially to get method, but posts params as JSON in post body.
|
157
189
|
def post(path, params = {})
|
158
|
-
|
190
|
+
resource(path).post(params)
|
159
191
|
end
|
160
192
|
|
161
193
|
# POST arbitrary REST API resource using an asynchronous http client.
|
162
194
|
# Works identially to get_async method, but posts params as JSON in post
|
163
195
|
# body.
|
164
196
|
def post_async(path, params = {})
|
165
|
-
|
197
|
+
resource(path).post_async(params)
|
166
198
|
end
|
167
199
|
|
168
200
|
## HELPER METHODS ##
|
@@ -176,18 +208,18 @@ module Pusher
|
|
176
208
|
WebHook.new(request, self)
|
177
209
|
end
|
178
210
|
|
179
|
-
# Return a convenience channel object by name
|
211
|
+
# Return a convenience channel object by name that delegates operations
|
212
|
+
# on a channel. No API request is made.
|
180
213
|
#
|
181
214
|
# @example
|
182
215
|
# Pusher['my-channel']
|
183
216
|
# @return [Channel]
|
184
|
-
# @raise [
|
185
|
-
#
|
217
|
+
# @raise [Pusher::Error] if the channel name is invalid.
|
218
|
+
# Channel names should be less than 200 characters, and
|
186
219
|
# should not contain anything other than letters, numbers, or the
|
187
220
|
# characters "_\-=@,.;"
|
188
221
|
def channel(channel_name)
|
189
|
-
|
190
|
-
Channel.new(url, channel_name, self)
|
222
|
+
Channel.new(nil, channel_name, self)
|
191
223
|
end
|
192
224
|
|
193
225
|
alias :[] :channel
|
@@ -223,7 +255,7 @@ module Pusher
|
|
223
255
|
get("/channels/#{channel_name}", params)
|
224
256
|
end
|
225
257
|
|
226
|
-
# Request info for users of a channel
|
258
|
+
# Request info for users of a presence channel
|
227
259
|
#
|
228
260
|
# GET /apps/[id]/channels/[channel_name]/users
|
229
261
|
#
|
@@ -258,6 +290,21 @@ module Pusher
|
|
258
290
|
post('/events', trigger_params(channels, event_name, data, params))
|
259
291
|
end
|
260
292
|
|
293
|
+
# Trigger multiple events at the same time
|
294
|
+
#
|
295
|
+
# POST /apps/[app_id]/batch_events
|
296
|
+
#
|
297
|
+
# @param events [Array] List of events to publish
|
298
|
+
#
|
299
|
+
# @return [Hash] See Pusher API docs
|
300
|
+
#
|
301
|
+
# @raise [Pusher::Error] Unsuccessful response - see the error message
|
302
|
+
# @raise [Pusher::HTTPError] Error raised inside http client. The original error is wrapped in error.original_error
|
303
|
+
#
|
304
|
+
def trigger_batch(*events)
|
305
|
+
post('/batch_events', trigger_batch_params(events.flatten))
|
306
|
+
end
|
307
|
+
|
261
308
|
# Trigger an event on one or more channels asynchronously.
|
262
309
|
# For parameters see #trigger
|
263
310
|
#
|
@@ -265,6 +312,14 @@ module Pusher
|
|
265
312
|
post_async('/events', trigger_params(channels, event_name, data, params))
|
266
313
|
end
|
267
314
|
|
315
|
+
# Trigger multiple events asynchronously.
|
316
|
+
# For parameters see #trigger_batch
|
317
|
+
#
|
318
|
+
def trigger_batch_async(*events)
|
319
|
+
post_async('/batch_events', trigger_batch_params(events.flatten))
|
320
|
+
end
|
321
|
+
|
322
|
+
|
268
323
|
# Generate the expected response for an authentication endpoint.
|
269
324
|
# See http://pusher.com/docs/authenticating_users for details.
|
270
325
|
#
|
@@ -285,18 +340,26 @@ module Pusher
|
|
285
340
|
#
|
286
341
|
# @return [Hash]
|
287
342
|
#
|
343
|
+
# @raise [Pusher::Error] if channel_name or socket_id are invalid
|
344
|
+
#
|
288
345
|
# @private Custom data is sent to server as JSON-encoded string
|
289
346
|
#
|
290
347
|
def authenticate(channel_name, socket_id, custom_data = nil)
|
291
348
|
channel_instance = channel(channel_name)
|
292
|
-
channel_instance.authenticate(socket_id, custom_data)
|
349
|
+
r = channel_instance.authenticate(socket_id, custom_data)
|
350
|
+
if channel_name.match(/^private-encrypted-/)
|
351
|
+
r[:shared_secret] = Base64.strict_encode64(
|
352
|
+
channel_instance.shared_secret(encryption_master_key)
|
353
|
+
)
|
354
|
+
end
|
355
|
+
r
|
293
356
|
end
|
294
357
|
|
295
358
|
# @private Construct a net/http http client
|
296
359
|
def sync_http_client
|
297
|
-
|
298
|
-
require 'httpclient'
|
360
|
+
require 'httpclient'
|
299
361
|
|
362
|
+
@client ||= begin
|
300
363
|
HTTPClient.new(@http_proxy).tap do |c|
|
301
364
|
c.connect_timeout = @connect_timeout
|
302
365
|
c.send_timeout = @send_timeout
|
@@ -315,14 +378,14 @@ module Pusher
|
|
315
378
|
require 'em-http' unless defined?(EventMachine::HttpRequest)
|
316
379
|
|
317
380
|
connection_opts = {
|
318
|
-
:
|
319
|
-
:
|
381
|
+
connect_timeout: @connect_timeout,
|
382
|
+
inactivity_timeout: @receive_timeout,
|
320
383
|
}
|
321
384
|
|
322
385
|
if defined?(@proxy)
|
323
386
|
proxy_opts = {
|
324
|
-
:
|
325
|
-
:
|
387
|
+
host: @proxy[:host],
|
388
|
+
port: @proxy[:port]
|
326
389
|
}
|
327
390
|
if @proxy[:user]
|
328
391
|
proxy_opts[:authorization] = [@proxy[:user], @proxy[:password]]
|
@@ -338,29 +401,73 @@ module Pusher
|
|
338
401
|
|
339
402
|
def trigger_params(channels, event_name, data, params)
|
340
403
|
channels = Array(channels).map(&:to_s)
|
341
|
-
raise Pusher::Error, "Too many channels (#{channels.length}), max
|
404
|
+
raise Pusher::Error, "Too many channels (#{channels.length}), max 100" if channels.length > 100
|
342
405
|
|
343
|
-
encoded_data =
|
344
|
-
|
345
|
-
data
|
406
|
+
encoded_data = if channels.any?{ |c| c.match(/^private-encrypted-/) } then
|
407
|
+
raise Pusher::Error, "Cannot trigger to multiple channels if any are encrypted" if channels.length > 1
|
408
|
+
encrypt(channels[0], encode_data(data))
|
346
409
|
else
|
347
|
-
|
348
|
-
MultiJson.encode(data)
|
349
|
-
rescue MultiJson::DecodeError => e
|
350
|
-
Pusher.logger.error("Could not convert #{data.inspect} into JSON")
|
351
|
-
raise e
|
352
|
-
end
|
410
|
+
encode_data(data)
|
353
411
|
end
|
354
412
|
|
355
|
-
|
356
|
-
:
|
357
|
-
:
|
358
|
-
:
|
413
|
+
params.merge({
|
414
|
+
name: event_name,
|
415
|
+
channels: channels,
|
416
|
+
data: encoded_data,
|
417
|
+
})
|
418
|
+
end
|
419
|
+
|
420
|
+
def trigger_batch_params(events)
|
421
|
+
{
|
422
|
+
batch: events.map do |event|
|
423
|
+
event.dup.tap do |e|
|
424
|
+
e[:data] = if e[:channel].match(/^private-encrypted-/) then
|
425
|
+
encrypt(e[:channel], encode_data(e[:data]))
|
426
|
+
else
|
427
|
+
encode_data(e[:data])
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
}
|
432
|
+
end
|
433
|
+
|
434
|
+
# JSON-encode the data if it's not a string
|
435
|
+
def encode_data(data)
|
436
|
+
return data if data.is_a? String
|
437
|
+
MultiJson.encode(data)
|
438
|
+
end
|
439
|
+
|
440
|
+
# Encrypts a message with a key derived from the master key and channel
|
441
|
+
# name
|
442
|
+
def encrypt(channel_name, encoded_data)
|
443
|
+
raise ConfigurationError, :encryption_master_key unless @encryption_master_key
|
444
|
+
|
445
|
+
# Only now load rbnacl, so that people that aren't using it don't need to
|
446
|
+
# install libsodium
|
447
|
+
require_rbnacl
|
448
|
+
|
449
|
+
secret_box = RbNaCl::SecretBox.new(
|
450
|
+
channel(channel_name).shared_secret(@encryption_master_key)
|
451
|
+
)
|
452
|
+
|
453
|
+
nonce = RbNaCl::Random.random_bytes(secret_box.nonce_bytes)
|
454
|
+
ciphertext = secret_box.encrypt(nonce, encoded_data)
|
455
|
+
|
456
|
+
MultiJson.encode({
|
457
|
+
"nonce" => Base64::strict_encode64(nonce),
|
458
|
+
"ciphertext" => Base64::strict_encode64(ciphertext),
|
359
459
|
})
|
360
460
|
end
|
361
461
|
|
362
462
|
def configured?
|
363
463
|
host && scheme && key && secret && app_id
|
364
464
|
end
|
465
|
+
|
466
|
+
def require_rbnacl
|
467
|
+
require 'rbnacl'
|
468
|
+
rescue LoadError => e
|
469
|
+
$stderr.puts "You don't have rbnacl installed in your application. Please add it to your Gemfile and run bundle install"
|
470
|
+
raise e
|
471
|
+
end
|
365
472
|
end
|
366
473
|
end
|
data/lib/pusher/request.rb
CHANGED
@@ -8,7 +8,9 @@ module Pusher
|
|
8
8
|
|
9
9
|
def initialize(client, verb, uri, params, body = nil)
|
10
10
|
@client, @verb, @uri = client, verb, uri
|
11
|
-
@head = {
|
11
|
+
@head = {
|
12
|
+
'X-Pusher-Library' => 'pusher-http-ruby ' + Pusher::VERSION
|
13
|
+
}
|
12
14
|
|
13
15
|
@body = body
|
14
16
|
if body
|
@@ -83,7 +85,7 @@ module Pusher
|
|
83
85
|
when 200
|
84
86
|
return symbolize_first_level(MultiJson.decode(body))
|
85
87
|
when 202
|
86
|
-
return true
|
88
|
+
return body.empty? ? true : symbolize_first_level(MultiJson.decode(body))
|
87
89
|
when 400
|
88
90
|
raise Error, "Bad request: #{body}"
|
89
91
|
when 401
|
@@ -92,6 +94,8 @@ module Pusher
|
|
92
94
|
raise Error, "404 Not found (#{@uri.path})"
|
93
95
|
when 407
|
94
96
|
raise Error, "Proxy Authentication Required"
|
97
|
+
when 413
|
98
|
+
raise Error, "Payload Too Large > 10KB"
|
95
99
|
else
|
96
100
|
raise Error, "Unknown error (status code #{status_code}): #{body}"
|
97
101
|
end
|
data/lib/pusher/webhook.rb
CHANGED
@@ -30,8 +30,8 @@ module Pusher
|
|
30
30
|
#
|
31
31
|
def initialize(request, client = Pusher)
|
32
32
|
@client = client
|
33
|
-
#
|
34
|
-
if
|
33
|
+
# For Rack::Request and ActionDispatch::Request
|
34
|
+
if request.respond_to?(:env) && request.respond_to?(:content_type)
|
35
35
|
@key = request.env['HTTP_X_PUSHER_KEY']
|
36
36
|
@signature = request.env["HTTP_X_PUSHER_SIGNATURE"]
|
37
37
|
@content_type = request.content_type
|
data/lib/pusher.rb
CHANGED
@@ -17,22 +17,30 @@ module Pusher
|
|
17
17
|
# end
|
18
18
|
class Error < RuntimeError; end
|
19
19
|
class AuthenticationError < Error; end
|
20
|
-
class ConfigurationError < Error
|
20
|
+
class ConfigurationError < Error
|
21
|
+
def initialize(key)
|
22
|
+
super "missing key `#{key}' in the client configuration"
|
23
|
+
end
|
24
|
+
end
|
21
25
|
class HTTPError < Error; attr_accessor :original_error; end
|
22
26
|
|
23
27
|
class << self
|
24
28
|
extend Forwardable
|
25
29
|
|
26
|
-
def_delegators :default_client, :scheme, :host, :port, :app_id, :key,
|
27
|
-
|
30
|
+
def_delegators :default_client, :scheme, :host, :port, :app_id, :key,
|
31
|
+
:secret, :http_proxy, :encryption_master_key_base64
|
32
|
+
def_delegators :default_client, :scheme=, :host=, :port=, :app_id=, :key=,
|
33
|
+
:secret=, :http_proxy=, :encryption_master_key_base64=
|
28
34
|
|
29
|
-
def_delegators :default_client, :authentication_token, :url
|
35
|
+
def_delegators :default_client, :authentication_token, :url, :cluster
|
30
36
|
def_delegators :default_client, :encrypted=, :url=, :cluster=
|
31
37
|
def_delegators :default_client, :timeout=, :connect_timeout=, :send_timeout=, :receive_timeout=, :keep_alive_timeout=
|
32
38
|
|
33
39
|
def_delegators :default_client, :get, :get_async, :post, :post_async
|
34
|
-
def_delegators :default_client, :channels, :channel_info, :channel_users
|
40
|
+
def_delegators :default_client, :channels, :channel_info, :channel_users
|
41
|
+
def_delegators :default_client, :trigger, :trigger_batch, :trigger_async, :trigger_batch_async
|
35
42
|
def_delegators :default_client, :authenticate, :webhook, :channel, :[]
|
43
|
+
def_delegators :default_client, :notify
|
36
44
|
|
37
45
|
attr_writer :logger
|
38
46
|
|
@@ -45,15 +53,15 @@ module Pusher
|
|
45
53
|
end
|
46
54
|
|
47
55
|
def default_client
|
48
|
-
@default_client ||=
|
56
|
+
@default_client ||= begin
|
57
|
+
cli = Pusher::Client
|
58
|
+
ENV['PUSHER_URL'] ? cli.from_env : cli.new
|
59
|
+
end
|
49
60
|
end
|
50
61
|
end
|
51
|
-
|
52
|
-
if ENV['PUSHER_URL']
|
53
|
-
self.url = ENV['PUSHER_URL']
|
54
|
-
end
|
55
62
|
end
|
56
63
|
|
64
|
+
require 'pusher/version'
|
57
65
|
require 'pusher/channel'
|
58
66
|
require 'pusher/request'
|
59
67
|
require 'pusher/resource'
|