pusher 0.17.0 → 2.0.3

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