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.
@@ -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'
@@ -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, :notification_host, :notification_scheme
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
- default_options = {
27
- :scheme => 'http',
28
- :port => 80,
29
- }
32
+ @scheme = "https"
33
+ @port = options[:port] || 443
30
34
 
31
- if options[:use_tls] || options[:encrypted]
32
- default_options[:scheme] = "https"
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
- merged_options = default_options.merge(options)
37
-
38
- if options.has_key?(:host)
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
- # TODO: Change host name when finalized
47
- merged_options[:notification_host] =
48
- options.fetch(:notification_host, "nativepush-cluster1.pusher.com")
44
+ @app_id = options[:app_id]
45
+ @key = options[:key]
46
+ @secret = options[:secret]
49
47
 
50
- merged_options[:notification_scheme] =
51
- options.fetch(:notification_scheme, "https")
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
- @scheme, @host, @port, @app_id, @key, @secret, @notification_host, @notification_scheme =
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 = nil
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 = 5
63
- @send_timeout = 5
64
- @receive_timeout = 5
65
- @keep_alive_timeout = 30
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
- :scheme => @scheme,
80
- :host => @host,
81
- :port => @port,
82
- :path => "/apps/#{@app_id}#{path}"
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
- :scheme => uri.scheme,
107
- :host => uri.host,
108
- :port => uri.port,
109
- :user => uri.user,
110
- :password => uri.password
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
- @client ||= begin
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
- :connect_timeout => @connect_timeout,
392
- :inactivity_timeout => @receive_timeout,
381
+ connect_timeout: @connect_timeout,
382
+ inactivity_timeout: @receive_timeout,
393
383
  }
394
384
 
395
385
  if defined?(@proxy)
396
386
  proxy_opts = {
397
- :host => @proxy[:host],
398
- :port => @proxy[:port]
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: encode_data(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] = encode_data(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
@@ -1,3 +1,3 @@
1
1
  module Pusher
2
- VERSION = '1.3.3'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -0,0 +1,7 @@
1
+ ## Description
2
+
3
+ Add a short description of the change. If this is related to an issue, please add a reference to the issue.
4
+
5
+ ## CHANGELOG
6
+
7
+ * [CHANGED] Describe your change here. Look at CHANGELOG.md to see the format.
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.add_dependency "multi_json", "~> 1.0"
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.7"
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.0"
22
- s.add_development_dependency "webmock"
23
- s.add_development_dependency "em-http-request", "~> 1.1.0"
24
- s.add_development_dependency "addressable", "=2.4.0"
25
- s.add_development_dependency "rake", "~> 10.4.2"
26
- s.add_development_dependency "rack", "~> 1.6.4"
27
- s.add_development_dependency "json", "~> 1.8.3"
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 = Pusher::Channel.new(@client.url, 'test_channel', @client)
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 = Pusher::Channel.new(@client.url, 'test_channel', @client)
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