pusher 1.3.3 → 2.0.0

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