Skip to content
Merged
10 changes: 5 additions & 5 deletions .github/workflows/gh-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Setup git
run: |
git config user.email "pusher-ci@pusher.com"
Expand All @@ -24,12 +24,12 @@ jobs:
export TAG=$(head -1 CHANGELOG.tmp | cut -d' ' -f2)
echo "TAG=$TAG" >> $GITHUB_ENV
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: softprops/action-gh-release@v2.6.1
with:
tag_name: ${{ env.TAG }}
release_name: ${{ env.TAG }}
name: ${{ env.TAG }}
body_path: CHANGELOG.tmp
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4

- name: Publish gem
uses: dawidd6/action-publish-gem@v1
uses: dawidd6/action-publish-gem@v1.2.0
with:
api_key: ${{secrets.RUBYGEMS_API_KEY}}
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
export NEW_VERSION=$(semver bump ${{ env.RELEASE }} $CURRENT)
echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Setup git
run: |
git config user.email "pusher-ci@pusher.com"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ on:

jobs:
test:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby: ['2.6', '2.7', '3.0']
ruby: ['3.2', '3.3', '3.4']

name: Ruby ${{ matrix.ruby }} Test

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Setup Ruby
uses: ruby/setup-ruby@v1
Expand Down
18 changes: 11 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
# Changelog

## 2.0.4

* [ADDED] Add `authenticate_user` method for user authentication flow

## 2.0.3

* [FIXED] Corrected the channels limit when publishing events. Upped from 10 to 100.

## 2.0.2

* [CHANGED] made encryption_master_key_base64 globally configurable
* [CHANGED] made encryption_master_key_base64 globally configurable

## 2.0.1

* [CHANGED] Only include lib and essential docs in gem.
* [CHANGED] Only include lib and essential docs in gem.

## 2.0.0

* [CHANGED] Use TLS by default.
* [REMOVED] Support for Ruby 2.4 and 2.5.
* [FIXED] Handle empty or nil configuration.
* [REMOVED] Legacy Push Notification integration.
* [ADDED] Stalebot and Github actions.
* [CHANGED] Use TLS by default.
* [REMOVED] Support for Ruby 2.4 and 2.5.
* [FIXED] Handle empty or nil configuration.
* [REMOVED] Legacy Push Notification integration.
* [ADDED] Stalebot and Github actions.

## 1.4.3

Expand Down
1 change: 1 addition & 0 deletions lib/pusher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'uri'
require 'forwardable'

require 'pusher/utils'
require 'pusher/client'

# Used for configuring API credentials and creating Channel objects
Expand Down
21 changes: 3 additions & 18 deletions lib/pusher/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, custom_string)
end

# Generate the expected response for an authentication endpoint.
Expand Down Expand Up @@ -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
57 changes: 56 additions & 1 deletion lib/pusher/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
34 changes: 34 additions & 0 deletions lib/pusher/utils.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion lib/pusher/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Pusher
VERSION = '2.0.3'
VERSION = '2.0.4'
end
18 changes: 18 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading