pusher 0.17.0 → 2.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|