pusher 0.17.0 → 2.0.3

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.
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
- default_options = {
14
- :scheme => 'http',
15
- :port => 80,
16
- }
17
- merged_options = default_options.merge(options)
32
+ @scheme = "https"
33
+ @port = options[:port] || 443
18
34
 
19
- if options.has_key?(:host)
20
- merged_options[:host] = options[:host]
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
- @scheme, @host, @port, @app_id, @key, @secret = merged_options.values_at(
28
- :scheme, :host, :port, :app_id, :key, :secret
29
- )
39
+ if options[:use_tls] == false || options[:encrypted] == false
40
+ @scheme = "http"
41
+ @port = options[:port] || 80
42
+ end
30
43
 
31
- @http_proxy = nil
32
- self.http_proxy = options[:http_proxy] if options[:http_proxy]
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 = 5
36
- @send_timeout = 5
37
- @receive_timeout = 5
38
- @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
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
- :scheme => @scheme,
50
- :host => @host,
51
- :port => @port,
52
- :path => "/apps/#{@app_id}#{path}"
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
- :scheme => uri.scheme,
77
- :host => uri.host,
78
- :port => uri.port,
79
- :user => uri.user,
80
- :password => uri.password
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
- ## INTERACE WITH THE API ##
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
- Resource.new(self, path).get(params)
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
- Resource.new(self, path).get_async(params)
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
- Resource.new(self, path).post(params)
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
- Resource.new(self, path).post_async(params)
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. No API request is made.
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 [ConfigurationError] unless key, secret and app_id have been
185
- # configured. Channel names should be less than 200 characters, and
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
- raise ConfigurationError, 'Missing client configuration: please check that key, secret and app_id are configured.' unless configured?
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
- @client ||= begin
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
- :connect_timeout => @connect_timeout,
319
- :inactivity_timeout => @receive_timeout,
381
+ connect_timeout: @connect_timeout,
382
+ inactivity_timeout: @receive_timeout,
320
383
  }
321
384
 
322
385
  if defined?(@proxy)
323
386
  proxy_opts = {
324
- :host => @proxy[:host],
325
- :port => @proxy[:port]
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 10" if channels.length > 10
404
+ raise Pusher::Error, "Too many channels (#{channels.length}), max 100" if channels.length > 100
342
405
 
343
- encoded_data = case data
344
- when String
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
- begin
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
- return params.merge({
356
- :name => event_name,
357
- :channels => channels,
358
- :data => encoded_data,
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Pusher
2
+ VERSION = '2.0.3'
3
+ end
@@ -30,8 +30,8 @@ module Pusher
30
30
  #
31
31
  def initialize(request, client = Pusher)
32
32
  @client = client
33
- # Should work without Rack
34
- if defined?(Rack::Request) && request.kind_of?(Rack::Request)
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; end
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, :secret, :http_proxy
27
- def_delegators :default_client, :scheme=, :host=, :port=, :app_id=, :key=, :secret=, :http_proxy=
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, :trigger, :trigger_async
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 ||= Pusher::Client.new
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'