pusher 1.3.3 → 2.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 +5 -5
- data/.github/stale.yml +26 -0
- data/.github/workflows/gh-release.yml +35 -0
- data/.github/workflows/publish.yml +17 -0
- data/.github/workflows/release.yml +71 -0
- data/.github/workflows/test.yml +31 -0
- data/CHANGELOG.md +84 -66
- data/README.md +76 -24
- data/examples/presence_channels/presence_channels.rb +56 -0
- data/examples/presence_channels/public/presence_channels.html +28 -0
- data/lib/pusher.rb +0 -3
- data/lib/pusher/channel.rb +9 -0
- data/lib/pusher/client.rb +98 -68
- data/lib/pusher/version.rb +1 -1
- data/pull_request_template.md +7 -0
- data/pusher.gemspec +12 -9
- data/spec/channel_spec.rb +22 -5
- data/spec/client_spec.rb +169 -96
- metadata +52 -39
- data/.document +0 -5
- data/.gemtest +0 -0
- data/.travis.yml +0 -16
- data/lib/pusher/native_notification/client.rb +0 -69
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sinatra/cookies'
|
3
|
+
require 'sinatra/json'
|
4
|
+
require 'pusher'
|
5
|
+
|
6
|
+
# You can get these variables from http://dashboard.pusher.com
|
7
|
+
pusher = Pusher::Client.new(
|
8
|
+
app_id: 'your-app-id',
|
9
|
+
key: 'your-app-key',
|
10
|
+
secret: 'your-app-secret',
|
11
|
+
cluster: 'your-app-cluster'
|
12
|
+
)
|
13
|
+
|
14
|
+
set :public_folder, 'public'
|
15
|
+
|
16
|
+
get '/' do
|
17
|
+
redirect '/presence_channels.html'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Emulate rails behaviour where this information would be stored in session
|
21
|
+
get '/signin' do
|
22
|
+
cookies[:user_id] = 'example_cookie'
|
23
|
+
'Ok'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Auth endpoint: https://pusher.com/docs/channels/server_api/authenticating-users
|
27
|
+
post '/pusher/auth' do
|
28
|
+
channel_data = {
|
29
|
+
user_id: 'example_user',
|
30
|
+
user_info: {
|
31
|
+
name: 'example_name',
|
32
|
+
email: 'example_email'
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
if cookies[:user_id] == 'example_cookie'
|
37
|
+
response = pusher.authenticate(params[:channel_name], params[:socket_id], channel_data)
|
38
|
+
json response
|
39
|
+
else
|
40
|
+
status 403
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
get '/pusher_trigger' do
|
45
|
+
channels = ['presence-channel-test'];
|
46
|
+
|
47
|
+
begin
|
48
|
+
pusher.trigger(channels, 'test-event', {
|
49
|
+
message: 'hello world'
|
50
|
+
})
|
51
|
+
rescue Pusher::Error => e
|
52
|
+
# For example, Pusher::AuthenticationError, Pusher::HTTPError, or Pusher::Error
|
53
|
+
end
|
54
|
+
|
55
|
+
'Triggered!'
|
56
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<head>
|
3
|
+
<title>Pusher Test</title>
|
4
|
+
<script src="https://js.pusher.com/5.0/pusher.min.js"></script>
|
5
|
+
<script>
|
6
|
+
|
7
|
+
// Enable pusher logging - don't include this in production
|
8
|
+
Pusher.logToConsole = true;
|
9
|
+
|
10
|
+
var pusher = new Pusher('your-app-key', {
|
11
|
+
cluster: 'your-app-cluster',
|
12
|
+
forceTLS: true,
|
13
|
+
authEndpoint: '/pusher/auth'
|
14
|
+
});
|
15
|
+
|
16
|
+
var channel = pusher.subscribe('presence-channel-test');
|
17
|
+
channel.bind('test-event', function(data) {
|
18
|
+
alert(JSON.stringify(data));
|
19
|
+
});
|
20
|
+
</script>
|
21
|
+
</head>
|
22
|
+
<body>
|
23
|
+
<h1>Pusher Test</h1>
|
24
|
+
<p>
|
25
|
+
Try publishing an event to channel <code>presence-channel-test</code>
|
26
|
+
with event name <code>test-event</code>.
|
27
|
+
</p>
|
28
|
+
</body>
|
data/lib/pusher.rb
CHANGED
@@ -28,9 +28,7 @@ module Pusher
|
|
28
28
|
extend Forwardable
|
29
29
|
|
30
30
|
def_delegators :default_client, :scheme, :host, :port, :app_id, :key, :secret, :http_proxy
|
31
|
-
def_delegators :default_client, :notification_host, :notification_scheme
|
32
31
|
def_delegators :default_client, :scheme=, :host=, :port=, :app_id=, :key=, :secret=, :http_proxy=
|
33
|
-
def_delegators :default_client, :notification_host=, :notification_scheme=
|
34
32
|
|
35
33
|
def_delegators :default_client, :authentication_token, :url, :cluster
|
36
34
|
def_delegators :default_client, :encrypted=, :url=, :cluster=
|
@@ -66,4 +64,3 @@ require 'pusher/channel'
|
|
66
64
|
require 'pusher/request'
|
67
65
|
require 'pusher/resource'
|
68
66
|
require 'pusher/webhook'
|
69
|
-
require 'pusher/native_notification/client'
|
data/lib/pusher/channel.rb
CHANGED
@@ -174,6 +174,15 @@ module Pusher
|
|
174
174
|
r
|
175
175
|
end
|
176
176
|
|
177
|
+
def shared_secret(encryption_master_key)
|
178
|
+
return unless encryption_master_key
|
179
|
+
|
180
|
+
secret_string = @name + encryption_master_key
|
181
|
+
digest = OpenSSL::Digest::SHA256.new
|
182
|
+
digest << secret_string
|
183
|
+
digest.digest
|
184
|
+
end
|
185
|
+
|
177
186
|
private
|
178
187
|
|
179
188
|
def validate_socket_id(socket_id)
|
data/lib/pusher/client.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
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"
|
11
17
|
|
12
18
|
# Loads the configuration from an url in the environment
|
13
19
|
def self.from_env(key = 'PUSHER_URL')
|
@@ -23,46 +29,35 @@ module Pusher
|
|
23
29
|
end
|
24
30
|
|
25
31
|
def initialize(options = {})
|
26
|
-
|
27
|
-
|
28
|
-
:port => 80,
|
29
|
-
}
|
32
|
+
@scheme = "https"
|
33
|
+
@port = options[:port] || 443
|
30
34
|
|
31
|
-
if options
|
32
|
-
|
33
|
-
default_options[:port] = 443
|
35
|
+
if options.key?(:encrypted)
|
36
|
+
warn "[DEPRECATION] `encrypted` is deprecated and will be removed in the next major version. Use `use_tls` instead."
|
34
37
|
end
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
merged_options[:host] = options[:host]
|
40
|
-
elsif options.has_key?(:cluster)
|
41
|
-
merged_options[:host] = "api-#{options[:cluster]}.pusher.com"
|
42
|
-
else
|
43
|
-
merged_options[:host] = "api.pusherapp.com"
|
39
|
+
if options[:use_tls] == false || options[:encrypted] == false
|
40
|
+
@scheme = "http"
|
41
|
+
@port = options[:port] || 80
|
44
42
|
end
|
45
43
|
|
46
|
-
|
47
|
-
|
48
|
-
|
44
|
+
@app_id = options[:app_id]
|
45
|
+
@key = options[:key]
|
46
|
+
@secret = options[:secret]
|
49
47
|
|
50
|
-
|
51
|
-
|
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"
|
52
51
|
|
53
|
-
@
|
54
|
-
merged_options.values_at(
|
55
|
-
:scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme
|
56
|
-
)
|
52
|
+
@encryption_master_key = Base64.strict_decode64(options[:encryption_master_key_base64]) if options[:encryption_master_key_base64]
|
57
53
|
|
58
|
-
@http_proxy =
|
59
|
-
self.http_proxy = options[:http_proxy] if options[:http_proxy]
|
54
|
+
@http_proxy = options[:http_proxy]
|
60
55
|
|
61
56
|
# Default timeouts
|
62
|
-
@connect_timeout =
|
63
|
-
@send_timeout =
|
64
|
-
@receive_timeout =
|
65
|
-
@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
|
66
61
|
end
|
67
62
|
|
68
63
|
# @private Returns the authentication token for the client
|
@@ -76,10 +71,10 @@ module Pusher
|
|
76
71
|
def url(path = nil)
|
77
72
|
raise ConfigurationError, :app_id unless @app_id
|
78
73
|
URI::Generic.build({
|
79
|
-
:
|
80
|
-
:
|
81
|
-
:
|
82
|
-
:
|
74
|
+
scheme: @scheme,
|
75
|
+
host: @host,
|
76
|
+
port: @port,
|
77
|
+
path: "/apps/#{@app_id}#{path}"
|
83
78
|
})
|
84
79
|
end
|
85
80
|
|
@@ -103,13 +98,12 @@ module Pusher
|
|
103
98
|
@http_proxy = http_proxy
|
104
99
|
uri = URI.parse(http_proxy)
|
105
100
|
@proxy = {
|
106
|
-
:
|
107
|
-
:
|
108
|
-
:
|
109
|
-
:
|
110
|
-
:
|
101
|
+
scheme: uri.scheme,
|
102
|
+
host: uri.host,
|
103
|
+
port: uri.port,
|
104
|
+
user: uri.user,
|
105
|
+
password: uri.password
|
111
106
|
}
|
112
|
-
@http_proxy
|
113
107
|
end
|
114
108
|
|
115
109
|
# Configure whether Pusher API calls should be made over SSL
|
@@ -129,6 +123,8 @@ module Pusher
|
|
129
123
|
end
|
130
124
|
|
131
125
|
def cluster=(cluster)
|
126
|
+
cluster = DEFAULT_CLUSTER if cluster.nil? || cluster.empty?
|
127
|
+
|
132
128
|
@host = "api-#{cluster}.pusher.com"
|
133
129
|
end
|
134
130
|
|
@@ -138,6 +134,12 @@ module Pusher
|
|
138
134
|
@connect_timeout, @send_timeout, @receive_timeout = value, value, value
|
139
135
|
end
|
140
136
|
|
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
|
+
|
141
143
|
## INTERACT WITH THE API ##
|
142
144
|
|
143
145
|
def resource(path)
|
@@ -317,24 +319,6 @@ module Pusher
|
|
317
319
|
post_async('/batch_events', trigger_batch_params(events.flatten))
|
318
320
|
end
|
319
321
|
|
320
|
-
def notification_client
|
321
|
-
@notification_client ||=
|
322
|
-
NativeNotification::Client.new(@app_id, @notification_host, @notification_scheme, self)
|
323
|
-
end
|
324
|
-
|
325
|
-
|
326
|
-
# Send a push notification
|
327
|
-
#
|
328
|
-
# POST /apps/[app_id]/notifications
|
329
|
-
#
|
330
|
-
# @param interests [Array] An array of interests
|
331
|
-
# @param message [String] Message to send
|
332
|
-
# @param options [Hash] Additional platform specific options
|
333
|
-
#
|
334
|
-
# @return [Hash]
|
335
|
-
def notify(interests, data = {})
|
336
|
-
notification_client.notify(interests, data)
|
337
|
-
end
|
338
322
|
|
339
323
|
# Generate the expected response for an authentication endpoint.
|
340
324
|
# See http://pusher.com/docs/authenticating_users for details.
|
@@ -362,14 +346,20 @@ module Pusher
|
|
362
346
|
#
|
363
347
|
def authenticate(channel_name, socket_id, custom_data = nil)
|
364
348
|
channel_instance = channel(channel_name)
|
365
|
-
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
|
366
356
|
end
|
367
357
|
|
368
358
|
# @private Construct a net/http http client
|
369
359
|
def sync_http_client
|
370
|
-
|
371
|
-
require 'httpclient'
|
360
|
+
require 'httpclient'
|
372
361
|
|
362
|
+
@client ||= begin
|
373
363
|
HTTPClient.new(@http_proxy).tap do |c|
|
374
364
|
c.connect_timeout = @connect_timeout
|
375
365
|
c.send_timeout = @send_timeout
|
@@ -388,14 +378,14 @@ module Pusher
|
|
388
378
|
require 'em-http' unless defined?(EventMachine::HttpRequest)
|
389
379
|
|
390
380
|
connection_opts = {
|
391
|
-
:
|
392
|
-
:
|
381
|
+
connect_timeout: @connect_timeout,
|
382
|
+
inactivity_timeout: @receive_timeout,
|
393
383
|
}
|
394
384
|
|
395
385
|
if defined?(@proxy)
|
396
386
|
proxy_opts = {
|
397
|
-
:
|
398
|
-
:
|
387
|
+
host: @proxy[:host],
|
388
|
+
port: @proxy[:port]
|
399
389
|
}
|
400
390
|
if @proxy[:user]
|
401
391
|
proxy_opts[:authorization] = [@proxy[:user], @proxy[:password]]
|
@@ -413,10 +403,17 @@ module Pusher
|
|
413
403
|
channels = Array(channels).map(&:to_s)
|
414
404
|
raise Pusher::Error, "Too many channels (#{channels.length}), max 10" if channels.length > 10
|
415
405
|
|
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))
|
409
|
+
else
|
410
|
+
encode_data(data)
|
411
|
+
end
|
412
|
+
|
416
413
|
params.merge({
|
417
414
|
name: event_name,
|
418
415
|
channels: channels,
|
419
|
-
data:
|
416
|
+
data: encoded_data,
|
420
417
|
})
|
421
418
|
end
|
422
419
|
|
@@ -424,7 +421,11 @@ module Pusher
|
|
424
421
|
{
|
425
422
|
batch: events.map do |event|
|
426
423
|
event.dup.tap do |e|
|
427
|
-
e[:data] =
|
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
|
428
429
|
end
|
429
430
|
end
|
430
431
|
}
|
@@ -436,8 +437,37 @@ module Pusher
|
|
436
437
|
MultiJson.encode(data)
|
437
438
|
end
|
438
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),
|
459
|
+
})
|
460
|
+
end
|
461
|
+
|
439
462
|
def configured?
|
440
463
|
host && scheme && key && secret && app_id
|
441
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
|
442
472
|
end
|
443
473
|
end
|
data/lib/pusher/version.rb
CHANGED
data/pusher.gemspec
CHANGED
@@ -13,18 +13,21 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.description = %q{Wrapper for Pusher Channels REST api: : https://pusher.com/channels}
|
14
14
|
s.license = "MIT"
|
15
15
|
|
16
|
-
s.
|
16
|
+
s.required_ruby_version = ">= 2.6"
|
17
|
+
|
18
|
+
s.add_dependency "multi_json", "~> 1.15"
|
17
19
|
s.add_dependency 'pusher-signature', "~> 0.1.8"
|
18
|
-
s.add_dependency "httpclient", "~> 2.
|
20
|
+
s.add_dependency "httpclient", "~> 2.8"
|
19
21
|
s.add_dependency "jruby-openssl" if defined?(JRUBY_VERSION)
|
20
22
|
|
21
|
-
s.add_development_dependency "rspec", "~> 3.
|
22
|
-
s.add_development_dependency "webmock"
|
23
|
-
s.add_development_dependency "em-http-request", "~> 1.1
|
24
|
-
s.add_development_dependency "addressable", "
|
25
|
-
s.add_development_dependency "rake", "~>
|
26
|
-
s.add_development_dependency "rack", "~>
|
27
|
-
s.add_development_dependency "json", "~>
|
23
|
+
s.add_development_dependency "rspec", "~> 3.9"
|
24
|
+
s.add_development_dependency "webmock", "~> 3.9"
|
25
|
+
s.add_development_dependency "em-http-request", "~> 1.1"
|
26
|
+
s.add_development_dependency "addressable", "~> 2.7"
|
27
|
+
s.add_development_dependency "rake", "~> 13.0"
|
28
|
+
s.add_development_dependency "rack", "~> 2.2"
|
29
|
+
s.add_development_dependency "json", "~> 2.3"
|
30
|
+
s.add_development_dependency "rbnacl", "~> 7.1"
|
28
31
|
|
29
32
|
s.files = `git ls-files`.split("\n")
|
30
33
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
data/spec/channel_spec.rb
CHANGED
@@ -29,7 +29,7 @@ describe Pusher::Channel do
|
|
29
29
|
describe '#trigger!' do
|
30
30
|
it "should use @client.trigger internally" do
|
31
31
|
expect(@client).to receive(:trigger)
|
32
|
-
@channel.trigger('new_event', 'Some data')
|
32
|
+
@channel.trigger!('new_event', 'Some data')
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -39,16 +39,14 @@ describe Pusher::Channel do
|
|
39
39
|
|
40
40
|
expect(Pusher.logger).to receive(:error).with("Exception from WebMock (HTTPClient::BadResponseError) (Pusher::HTTPError)")
|
41
41
|
expect(Pusher.logger).to receive(:debug) #backtrace
|
42
|
-
channel
|
43
|
-
channel.trigger('new_event', 'Some data')
|
42
|
+
@channel.trigger('new_event', 'Some data')
|
44
43
|
end
|
45
44
|
|
46
45
|
it "should log failure if Pusher returns an error response" do
|
47
46
|
stub_post 401, "some signature info"
|
48
47
|
expect(Pusher.logger).to receive(:error).with("some signature info (Pusher::AuthenticationError)")
|
49
48
|
expect(Pusher.logger).to receive(:debug) #backtrace
|
50
|
-
channel
|
51
|
-
channel.trigger('new_event', 'Some data')
|
49
|
+
@channel.trigger('new_event', 'Some data')
|
52
50
|
end
|
53
51
|
end
|
54
52
|
|
@@ -167,4 +165,23 @@ describe Pusher::Channel do
|
|
167
165
|
}.to raise_error Pusher::Error
|
168
166
|
end
|
169
167
|
end
|
168
|
+
|
169
|
+
describe `#shared_secret` do
|
170
|
+
before(:each) do
|
171
|
+
@channel.instance_variable_set(:@name, 'private-encrypted-1')
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should return a shared_secret based on the channel name and encryption master key' do
|
175
|
+
key = '3W1pfB/Etr+ZIlfMWwZP3gz8jEeCt4s2pe6Vpr+2c3M='
|
176
|
+
shared_secret = @channel.shared_secret(key)
|
177
|
+
expect(Base64.strict_encode64(shared_secret)).to eq(
|
178
|
+
"6zeEp/chneRPS1cbK/hGeG860UhHomxSN6hTgzwT20I="
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should return nil if missing encryption master key' do
|
183
|
+
shared_secret = @channel.shared_secret(nil)
|
184
|
+
expect(shared_secret).to be_nil
|
185
|
+
end
|
186
|
+
end
|
170
187
|
end
|