diff --git a/lib/pusher.rb b/lib/pusher.rb index 0c269ff..4537d93 100644 --- a/lib/pusher.rb +++ b/lib/pusher.rb @@ -2,6 +2,7 @@ require 'uri' require 'forwardable' +require 'pusher/utils' require 'pusher/client' # Used for configuring API credentials and creating Channel objects diff --git a/lib/pusher/channel.rb b/lib/pusher/channel.rb index e3f8c39..9c6de18 100644 --- a/lib/pusher/channel.rb +++ b/lib/pusher/channel.rb @@ -126,20 +126,9 @@ def users(params = {}) # @raise [Pusher::Error] if socket_id or custom_string invalid # def authentication_string(socket_id, custom_string = nil) - validate_socket_id(socket_id) + string_to_sign = [socket_id, name, custom_string].compact.map(&:to_s).join(':') - unless custom_string.nil? || custom_string.kind_of?(String) - raise Error, 'Custom argument must be a string' - end - - string_to_sign = [socket_id, name, custom_string]. - compact.map(&:to_s).join(':') - Pusher.logger.debug "Signing #{string_to_sign}" - token = @client.authentication_token - digest = OpenSSL::Digest::SHA256.new - signature = OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign) - - return "#{token.key}:#{signature}" + _authentication_string(socket_id, string_to_sign, @client.authentication_token, string_to_sign) end # Generate the expected response for an authentication endpoint. @@ -185,10 +174,6 @@ def shared_secret(encryption_master_key) private - def validate_socket_id(socket_id) - unless socket_id && /\A\d+\.\d+\z/.match(socket_id) - raise Pusher::Error, "Invalid socket ID #{socket_id.inspect}" - end - end + include Pusher::Utils end end diff --git a/lib/pusher/client.rb b/lib/pusher/client.rb index bb7960b..0de5841 100644 --- a/lib/pusher/client.rb +++ b/lib/pusher/client.rb @@ -321,7 +321,7 @@ def trigger_batch_async(*events) # Generate the expected response for an authentication endpoint. - # See http://pusher.com/docs/authenticating_users for details. + # See https://pusher.com/docs/channels/server_api/authorizing-users for details. # # @example Private channels # render :json => Pusher.authenticate('private-my_channel', params[:socket_id]) @@ -355,6 +355,31 @@ def authenticate(channel_name, socket_id, custom_data = nil) r end + # Generate the expected response for a user authentication endpoint. + # See https://pusher.com/docs/authenticating_users for details. + # + # @example + # user_data = { id: current_user.id.to_s, company_id: current_user.company_id } + # render :json => Pusher.authenticate_user(params[:socket_id], user_data) + # + # @param socket_id [String] + # @param user_data [Hash] user's properties (id is required and must be a string) + # + # @return [Hash] + # + # @raise [Pusher::Error] if socket_id or user_data is invalid + # + # @private Custom data is sent to server as JSON-encoded string + # + def authenticate_user(socket_id, user_data) + validate_user_data(user_data) + + custom_data = MultiJson.encode(user_data) + auth = authentication_string(socket_id, custom_data) + + { auth:, user_data: custom_data } + end + # @private Construct a net/http http client def sync_http_client require 'httpclient' @@ -399,6 +424,8 @@ def em_http_client(uri) private + include Pusher::Utils + def trigger_params(channels, event_name, data, params) channels = Array(channels).map(&:to_s) raise Pusher::Error, "Too many channels (#{channels.length}), max 100" if channels.length > 100 @@ -469,5 +496,33 @@ def require_rbnacl $stderr.puts "You don't have rbnacl installed in your application. Please add it to your Gemfile and run bundle install" raise e end + + # Compute authentication string required as part of the user authentication + # endpoint response. Generally the authenticate method should be used in + # preference to this one. + # + # @param socket_id [String] Each Pusher socket connection receives a + # unique socket_id. This is sent from pusher.js to your server when + # channel authentication is required. + # @param custom_string [String] Allows signing additional data + # @return [String] + # + # @raise [Pusher::Error] if socket_id or custom_string invalid + # + def authentication_string(socket_id, custom_string = nil) + string_to_sign = [socket_id, 'user', custom_string].compact.map(&:to_s).join('::') + + _authentication_string(socket_id, string_to_sign, authentication_token, string_to_sign) + end + + def validate_user_data(user_data) + return if user_data_valid?(user_data) + + raise Pusher::Error, "Invalid user data #{user_data.inspect}" + end + + def user_data_valid?(data) + data.is_a?(Hash) && data.key?(:id) && !data[:id].empty? && data[:id].is_a?(String) + end end end diff --git a/lib/pusher/utils.rb b/lib/pusher/utils.rb new file mode 100644 index 0000000..7fffffd --- /dev/null +++ b/lib/pusher/utils.rb @@ -0,0 +1,34 @@ +module Pusher + module Utils + def validate_socket_id(socket_id) + unless socket_id && /\A\d+\.\d+\z/.match(socket_id) + raise Pusher::Error, "Invalid socket ID #{socket_id.inspect}" + end + end + + # Compute authentication string required as part of the user authentication + # and subscription authorization endpoints responses. + # Generally the authenticate method should be used in preference to this one. + # + # @param socket_id [String] Each Pusher socket connection receives a + # unique socket_id. This is sent from pusher.js to your server when + # channel authentication is required. + # @param custom_string [String] Allows signing additional data + # @return [String] + # + # @raise [Pusher::Error] if socket_id or custom_string invalid + # + def _authentication_string(socket_id, string_to_sign, token, custom_string = nil) + validate_socket_id(socket_id) + + raise Pusher::Error, 'Custom argument must be a string' unless custom_string.nil? || custom_string.is_a?(String) + + Pusher.logger.debug "Signing #{string_to_sign}" + + digest = OpenSSL::Digest.new('SHA256') + signature = OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign) + + "#{token.key}:#{signature}" + end + end +end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index f374d1f..474f83e 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -310,6 +310,24 @@ end + describe '#authenticate_user' do + before :each do + @user_data = {:id => '123', :foo => { :name => 'Bar' }} + end + + it 'should return a hash with signature including custom data and data as json string' do + allow(MultiJson).to receive(:encode).with(@user_data).and_return 'a json string' + + response = @client.authenticate_user('1.1', @user_data) + + expect(response).to eq({ + :auth => "12345678900000001:#{hmac(@client.secret, "1.1::user::a json string")}", + :user_data => 'a json string' + }) + end + + end + describe '#trigger' do before :each do @api_path = %r{/apps/20/events}