Fixed issue #2327 - Websocket messages are not working correctly via Ajax long polling (e. g. multiple browser tabs can get opened with one session).

This commit is contained in:
Martin Edenhofer 2018-11-02 18:42:57 +01:00
parent 764d262cff
commit 75230c3db3
15 changed files with 303 additions and 51 deletions

View file

@ -35,6 +35,11 @@ class App.WebSocket
_instance ?= new _webSocketSingleton _instance ?= new _webSocketSingleton
_instance.support() _instance.support()
@queue: ->
if _instance == undefined
_instance ?= new _webSocketSingleton
_instance.queue
# The actual Singleton class # The actual Singleton class
class _webSocketSingleton extends App.Controller class _webSocketSingleton extends App.Controller
@include App.LogInclude @include App.LogInclude
@ -355,10 +360,10 @@ class _webSocketSingleton extends App.Controller
success: (data) => success: (data) =>
if data && data.error if data && data.error
@client_id = undefined @client_id = undefined
@_ajaxInit( force: true ) @_ajaxInit(force: true)
error: => error: =>
@client_id = undefined @client_id = undefined
@_ajaxInit( force: true ) @_ajaxInit(force: true)
) )
_ajaxReceive: => _ajaxReceive: =>

View file

@ -2,6 +2,7 @@
class LongPollingController < ApplicationController class LongPollingController < ApplicationController
skip_before_action :session_update # prevent race conditions skip_before_action :session_update # prevent race conditions
prepend_before_action :authentication_check_only
# GET /api/v1/message_send # GET /api/v1/message_send
def message_send def message_send
@ -14,24 +15,22 @@ class LongPollingController < ApplicationController
client_id = client_id_gen client_id = client_id_gen
log 'new client connection', client_id log 'new client connection', client_id
end end
if !params['data'] data = params['data'].permit!.to_h
params['data'] = {}
end
session_data = {} session_data = {}
if current_user&.id if current_user&.id
session_data = { 'id' => current_user.id } session_data = { 'id' => current_user.id }
end end
# spool messages for new connects # spool messages for new connects
if params['data']['spool'] if data['spool']
Sessions.spool_create(params['data']) Sessions.spool_create(data)
end end
if params['data']['event'] == 'login' if data['event'] == 'login'
Sessions.create(client_id, session_data, { type: 'ajax' }) Sessions.create(client_id, session_data, { type: 'ajax' })
elsif params['data']['event'] elsif data['event']
message = Sessions::Event.run( message = Sessions::Event.run(
event: params['data']['event'], event: data['event'],
payload: params['data'], payload: data,
session: session_data, session: session_data,
client_id: client_id, client_id: client_id,
clients: {}, clients: {},
@ -41,15 +40,15 @@ class LongPollingController < ApplicationController
Sessions.send(client_id, message) Sessions.send(client_id, message)
end end
else else
log "unknown message '#{params['data'].inspect}'", client_id log "unknown message '#{data.inspect}'", client_id
end end
if new_connection if new_connection
result = { client_id: client_id } result = { client_id: client_id }
render json: result render json: result
else return
render json: {}
end end
render json: {}
end end
# GET /api/v1/message_receive # GET /api/v1/message_receive
@ -110,7 +109,7 @@ class LongPollingController < ApplicationController
params[:client_id].to_s params[:client_id].to_s
end end
def log( data, client_id = '-' ) def log(data, client_id = '-')
logger.info "client(#{client_id}) #{data}" logger.info "client(#{client_id}) #{data}"
end end
end end

View file

@ -334,6 +334,15 @@ send message to recipient client
Sessions.send_to(user_id, data) Sessions.send_to(user_id, data)
e. g.
Sessions.send_to(user_id, {
event: 'session:takeover',
data: {
taskbar_id: 12312
},
})
returns returns
true|false true|false
@ -476,6 +485,14 @@ remove all session and spool messages
true true
end end
=begin
create spool messages
Sessions.spool_create(some: 'data')
=end
def self.spool_create(data) def self.spool_create(data)
msg = JSON.generate(data) msg = JSON.generate(data)
path = "#{@path}/spool/" path = "#{@path}/spool/"
@ -492,6 +509,14 @@ remove all session and spool messages
end end
end end
=begin
get spool messages
Sessions.spool_list(junger_then, for_user_id)
=end
def self.spool_list(timestamp, current_user_id) def self.spool_list(timestamp, current_user_id)
path = "#{@path}/spool/" path = "#{@path}/spool/"
FileUtils.mkpath path FileUtils.mkpath path
@ -512,6 +537,7 @@ remove all session and spool messages
file.flock(File::LOCK_SH) file.flock(File::LOCK_SH)
message = file.read message = file.read
file.flock(File::LOCK_UN) file.flock(File::LOCK_UN)
message_parsed = {}
begin begin
spool = JSON.parse(message) spool = JSON.parse(message)
message_parsed = JSON.parse(spool['msg']) message_parsed = JSON.parse(spool['msg'])
@ -530,7 +556,7 @@ remove all session and spool messages
# add spool attribute to push spool info to clients # add spool attribute to push spool info to clients
message_parsed['spool'] = true message_parsed['spool'] = true
# only send not already now messages # only send not already older messages
if !timestamp || timestamp < spool['timestamp'] if !timestamp || timestamp < spool['timestamp']
# spool to recipient list # spool to recipient list
@ -573,6 +599,19 @@ remove all session and spool messages
data data
end end
=begin
delete spool messages
Sessions.spool_delete
=end
def self.spool_delete
path = "#{@path}/spool/"
FileUtils.rm_rf path
end
def self.jobs(node_id = nil) def self.jobs(node_id = nil)
# just make sure that spool path exists # just make sure that spool path exists

View file

@ -7,7 +7,7 @@ class Sessions::Event
begin begin
backend = load_adapter(adapter) backend = load_adapter(adapter)
rescue => e rescue => e
return { event: 'error', data: { error: "No such event #{params[:event]}", payload: params[:payload] } } return { event: 'error', data: { error: "No such event #{params[:event]}: #{e.inspect}", payload: params[:payload] } }
end end
begin begin

View file

@ -9,6 +9,10 @@ class Sessions::Event::Base
return if !@clients[@client_id] return if !@clients[@client_id]
@is_web_socket = true @is_web_socket = true
return if !self.class.instance_variable_get(:@database_connection)
ActiveRecord::Base.establish_connection
end end
def websocket_send(recipient_client_id, data) def websocket_send(recipient_client_id, data)
@ -120,8 +124,18 @@ class Sessions::Event::Base
puts "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}" puts "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}"
#puts "#{Time.now.utc.iso8601}:#{ level }:client(#{ client_id }) #{ data }" #puts "#{Time.now.utc.iso8601}:#{ level }:client(#{ client_id }) #{ data }"
# rubocop:enable Rails/Output # rubocop:enable Rails/Output
#Rails.logger.info "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}"
end end
def destroy; end def self.database_connection_required
@database_connection = true
end
def destroy
return if !@is_web_socket
return if !self.class.instance_variable_get(:@database_connection)
ActiveRecord::Base.remove_connection
end
end end

View file

@ -1,5 +1,15 @@
class Sessions::Event::Broadcast < Sessions::Event::Base class Sessions::Event::Broadcast < Sessions::Event::Base
=begin
Event module to broadcast messages to all client connections.
To execute this manually, just paste the following into the browser console
App.WebSocket.send({event:'broadcast', recipient: { user_id: [1,2,3]}, data: {some: 'key'}})
=end
def run def run
# list all current clients # list all current clients

View file

@ -1,17 +1,5 @@
class Sessions::Event::ChatBase < Sessions::Event::Base class Sessions::Event::ChatBase < Sessions::Event::Base
database_connection_required
def initialize(params)
super(params)
return if !@is_web_socket
ActiveRecord::Base.establish_connection
end
def destroy
return if !@is_web_socket
ActiveRecord::Base.remove_connection
end
def run def run

View file

@ -1,12 +1,20 @@
class Sessions::Event::Login < Sessions::Event::Base class Sessions::Event::Login < Sessions::Event::Base
database_connection_required
=begin
Event module to start websocket session for new client connections.
To execute this manually, just paste the following into the browser console
App.WebSocket.send({event:'login', session_id: '123'})
=end
def run def run
# get user_id # get user_id
session = nil session = nil
if @is_web_socket
ActiveRecord::Base.establish_connection
end
app_version = AppVersion.event_data app_version = AppVersion.event_data
@ -14,12 +22,7 @@ class Sessions::Event::Login < Sessions::Event::Base
session = ActiveRecord::SessionStore::Session.find_by(session_id: @payload['session_id']) session = ActiveRecord::SessionStore::Session.find_by(session_id: @payload['session_id'])
end end
if @is_web_socket
ActiveRecord::Base.remove_connection
end
new_session_data = {} new_session_data = {}
if session&.data && session.data['user_id'] if session&.data && session.data['user_id']
new_session_data = { new_session_data = {
'id' => session.data['user_id'], 'id' => session.data['user_id'],

View file

@ -1,17 +1,15 @@
class Sessions::Event::Maintenance < Sessions::Event::Base class Sessions::Event::Maintenance < Sessions::Event::Base
database_connection_required
def initialize(params) =begin
super(params)
return if !@is_web_socket
ActiveRecord::Base.establish_connection Event module to broadcast maintenance messages to all client connections.
end
def destroy To execute this manually, just paste the following into the browser console
return if !@is_web_socket
ActiveRecord::Base.remove_connection App.WebSocket.send({event:'maintenance', data: {some: 'key'}})
end
=end
def run def run

View file

@ -1,5 +1,15 @@
class Sessions::Event::Ping < Sessions::Event::Base class Sessions::Event::Ping < Sessions::Event::Base
=begin
Event module to send pong to client connection.
To execute this manually, just paste the following into the browser console
App.WebSocket.send({event:'ping'})
=end
def run def run
{ {
event: 'pong', event: 'pong',

View file

@ -1,6 +1,15 @@
class Sessions::Event::Spool < Sessions::Event::Base class Sessions::Event::Spool < Sessions::Event::Base
# get spool messages and send them to new client connection =begin
Event module to serve spool messages and send them to new client connection.
To execute this manually, just paste the following into the browser console
App.WebSocket.send({event:'spool'})
=end
def run def run
# error handling # error handling
@ -11,8 +20,13 @@ class Sessions::Event::Spool < Sessions::Event::Base
end end
if !@session || !@session['id'] if !@session || !@session['id']
log 'error', "can't send spool, session not authenticated" log 'error', "Can't send spool, session not authenticated"
return return {
event: 'error',
data: {
error: 'Can\'t send spool, session not authenticated',
},
}
end end
spool = Sessions.spool_list(@payload['timestamp'], @session['id']) spool = Sessions.spool_list(@payload['timestamp'], @session['id'])

View file

@ -1,4 +1,5 @@
class Sessions::Event::TicketOverviewIndex < Sessions::Event::Base class Sessions::Event::TicketOverviewIndex < Sessions::Event::Base
database_connection_required
def run def run
return if !valid_session? return if !valid_session?

View file

@ -1,4 +1,5 @@
class Sessions::Event::TicketOverviewList < Sessions::Event::Base class Sessions::Event::TicketOverviewList < Sessions::Event::Base
database_connection_required
def run def run
return if !valid_session? return if !valid_session?

View file

@ -0,0 +1,46 @@
class Sessions::Event::WhoAmI < Sessions::Event::Base
database_connection_required
=begin
Event module to send `who am i` to client connection.
To execute this manually, just paste the following into the browser console
App.WebSocket.send({event:'who_am_i'})
=end
def run
if !@session || !@session['id']
return {
event: 'who_am_i',
data: {
message: 'session not authenticated',
},
}
end
user = User.find_by(id: @session['id'])
if !user
return {
event: 'who_am_i',
data: {
message: "No such user with id #{@session['id']}",
},
}
end
attributes = user.attributes
attributes.delete('password')
{
event: 'who_am_i',
data: {
message: 'session authenticated',
user: attributes,
},
}
end
end

View file

@ -0,0 +1,124 @@
require 'rails_helper'
RSpec.describe 'LongPolling', type: :request do
let(:agent_user) do
create(:agent_user)
end
before(:each) do
Sessions.sessions.each do |client_id|
Sessions.destroy(client_id)
end
Sessions.spool_delete
end
describe 'request handling' do
it 'receive without client_id - no user login' do
get '/api/v1/message_receive', params: { data: {} }, as: :json
expect(response).to have_http_status(422)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('Invalid client_id receive!')
end
it 'send without client_id - no user login' do
get '/api/v1/message_send', params: { data: {} }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
client_id = json_response['client_id']
get '/api/v1/message_send', params: { client_id: client_id, data: { event: 'spool' } }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
expect(response).to have_http_status(422)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('Invalid client_id receive!')
end
it 'receive without client_id' do
authenticated_as(agent_user)
get '/api/v1/message_receive', params: { data: {} }, as: :json
expect(response).to have_http_status(422)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('Invalid client_id receive!')
end
it 'receive without wrong client_id' do
authenticated_as(agent_user)
get '/api/v1/message_receive', params: { client_id: 'not existing', data: {} }, as: :json
expect(response).to have_http_status(422)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('Invalid client_id receive!')
end
it 'send without client_id' do
authenticated_as(agent_user)
get '/api/v1/message_send', params: { data: {} }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
end
it 'send with client_id' do
Sessions.create('123456', {}, { type: 'ajax' })
authenticated_as(agent_user)
get '/api/v1/message_send', params: { client_id: '123456', data: {} }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response).to eq({})
end
it 'send event spool and receive data' do
authenticated_as(agent_user)
get '/api/v1/message_send', params: { data: { event: 'login' } }, as: :json
expect(response).to have_http_status(200)
expect(json_response['client_id'].to_i).to be_between(1, 9_999_999_999)
client_id = json_response['client_id']
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Array)
expect(json_response).to eq([{ 'data' => { 'success' => true }, 'event' => 'ws:login' }])
get '/api/v1/message_send', params: { client_id: client_id, data: { event: 'spool' } }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response).to eq({})
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Array)
expect(json_response[0]['event']).to eq('spool:sent')
expect(json_response[0]['event']).to eq('spool:sent')
expect(json_response.count).to eq(1)
spool_list = Sessions.spool_list(Time.now.utc.to_i, agent_user.id)
expect(spool_list).to eq([])
get '/api/v1/message_send', params: { client_id: client_id, data: { event: 'broadcast', spool: true, recipient: { user_id: [agent_user.id] }, data: { taskbar_id: 9_391_633 } } }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response).to eq({})
get '/api/v1/message_receive', params: { client_id: client_id, data: {} }, as: :json
expect(response).to have_http_status(200)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response).to eq({ 'event' => 'pong' })
travel 2.seconds
spool_list = Sessions.spool_list(Time.now.utc.to_i, agent_user.id)
expect(spool_list).to eq([])
spool_list = Sessions.spool_list(nil, agent_user.id)
expect(spool_list).to eq([{ message: { 'taskbar_id' => 9_391_633 }, type: 'direct' }])
end
end
end