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