diff --git a/app/assets/javascripts/app/lib/app_post/websocket.coffee b/app/assets/javascripts/app/lib/app_post/websocket.coffee index 06e461cc3..e1bf02649 100644 --- a/app/assets/javascripts/app/lib/app_post/websocket.coffee +++ b/app/assets/javascripts/app/lib/app_post/websocket.coffee @@ -35,6 +35,11 @@ class App.WebSocket _instance ?= new _webSocketSingleton _instance.support() + @queue: -> + if _instance == undefined + _instance ?= new _webSocketSingleton + _instance.queue + # The actual Singleton class class _webSocketSingleton extends App.Controller @include App.LogInclude @@ -355,10 +360,10 @@ class _webSocketSingleton extends App.Controller success: (data) => if data && data.error @client_id = undefined - @_ajaxInit( force: true ) + @_ajaxInit(force: true) error: => @client_id = undefined - @_ajaxInit( force: true ) + @_ajaxInit(force: true) ) _ajaxReceive: => diff --git a/app/controllers/long_polling_controller.rb b/app/controllers/long_polling_controller.rb index 0c8e198c5..3c3d7c652 100644 --- a/app/controllers/long_polling_controller.rb +++ b/app/controllers/long_polling_controller.rb @@ -2,6 +2,7 @@ class LongPollingController < ApplicationController skip_before_action :session_update # prevent race conditions + prepend_before_action :authentication_check_only # GET /api/v1/message_send def message_send @@ -14,24 +15,22 @@ class LongPollingController < ApplicationController client_id = client_id_gen log 'new client connection', client_id end - if !params['data'] - params['data'] = {} - end + data = params['data'].permit!.to_h session_data = {} if current_user&.id session_data = { 'id' => current_user.id } end # spool messages for new connects - if params['data']['spool'] - Sessions.spool_create(params['data']) + if data['spool'] + Sessions.spool_create(data) end - if params['data']['event'] == 'login' + if data['event'] == 'login' Sessions.create(client_id, session_data, { type: 'ajax' }) - elsif params['data']['event'] + elsif data['event'] message = Sessions::Event.run( - event: params['data']['event'], - payload: params['data'], + event: data['event'], + payload: data, session: session_data, client_id: client_id, clients: {}, @@ -41,15 +40,15 @@ class LongPollingController < ApplicationController Sessions.send(client_id, message) end else - log "unknown message '#{params['data'].inspect}'", client_id + log "unknown message '#{data.inspect}'", client_id end if new_connection result = { client_id: client_id } render json: result - else - render json: {} + return end + render json: {} end # GET /api/v1/message_receive @@ -110,7 +109,7 @@ class LongPollingController < ApplicationController params[:client_id].to_s end - def log( data, client_id = '-' ) + def log(data, client_id = '-') logger.info "client(#{client_id}) #{data}" end end diff --git a/lib/sessions.rb b/lib/sessions.rb index 6df4549bd..05b82213e 100644 --- a/lib/sessions.rb +++ b/lib/sessions.rb @@ -334,6 +334,15 @@ send message to recipient client Sessions.send_to(user_id, data) +e. g. + + Sessions.send_to(user_id, { + event: 'session:takeover', + data: { + taskbar_id: 12312 + }, + }) + returns true|false @@ -476,6 +485,14 @@ remove all session and spool messages true end +=begin + +create spool messages + + Sessions.spool_create(some: 'data') + +=end + def self.spool_create(data) msg = JSON.generate(data) path = "#{@path}/spool/" @@ -492,6 +509,14 @@ remove all session and spool messages end end +=begin + +get spool messages + + Sessions.spool_list(junger_then, for_user_id) + +=end + def self.spool_list(timestamp, current_user_id) path = "#{@path}/spool/" FileUtils.mkpath path @@ -512,6 +537,7 @@ remove all session and spool messages file.flock(File::LOCK_SH) message = file.read file.flock(File::LOCK_UN) + message_parsed = {} begin spool = JSON.parse(message) 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 message_parsed['spool'] = true - # only send not already now messages + # only send not already older messages if !timestamp || timestamp < spool['timestamp'] # spool to recipient list @@ -573,6 +599,19 @@ remove all session and spool messages data 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) # just make sure that spool path exists diff --git a/lib/sessions/event.rb b/lib/sessions/event.rb index 3aa7f0b4b..2280fb205 100644 --- a/lib/sessions/event.rb +++ b/lib/sessions/event.rb @@ -7,7 +7,7 @@ class Sessions::Event begin backend = load_adapter(adapter) 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 begin diff --git a/lib/sessions/event/base.rb b/lib/sessions/event/base.rb index 3c42ee38f..26de179e4 100644 --- a/lib/sessions/event/base.rb +++ b/lib/sessions/event/base.rb @@ -9,6 +9,10 @@ class Sessions::Event::Base return if !@clients[@client_id] @is_web_socket = true + + return if !self.class.instance_variable_get(:@database_connection) + + ActiveRecord::Base.establish_connection end 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}:#{ level }:client(#{ client_id }) #{ data }" # rubocop:enable Rails/Output + #Rails.logger.info "#{Time.now.utc.iso8601}:client(#{client_id}) #{data}" 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 diff --git a/lib/sessions/event/broadcast.rb b/lib/sessions/event/broadcast.rb index 9d1c6818a..df1dbd2b5 100644 --- a/lib/sessions/event/broadcast.rb +++ b/lib/sessions/event/broadcast.rb @@ -1,5 +1,15 @@ 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 # list all current clients diff --git a/lib/sessions/event/chat_base.rb b/lib/sessions/event/chat_base.rb index 5bd3f150e..29978c3a8 100644 --- a/lib/sessions/event/chat_base.rb +++ b/lib/sessions/event/chat_base.rb @@ -1,17 +1,5 @@ class Sessions::Event::ChatBase < Sessions::Event::Base - - 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 + database_connection_required def run diff --git a/lib/sessions/event/login.rb b/lib/sessions/event/login.rb index 2533d08b8..d341684d1 100644 --- a/lib/sessions/event/login.rb +++ b/lib/sessions/event/login.rb @@ -1,12 +1,20 @@ 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 # get user_id session = nil - if @is_web_socket - ActiveRecord::Base.establish_connection - end 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']) end - if @is_web_socket - ActiveRecord::Base.remove_connection - end - new_session_data = {} - if session&.data && session.data['user_id'] new_session_data = { 'id' => session.data['user_id'], diff --git a/lib/sessions/event/maintenance.rb b/lib/sessions/event/maintenance.rb index 46a5317aa..c4b18d078 100644 --- a/lib/sessions/event/maintenance.rb +++ b/lib/sessions/event/maintenance.rb @@ -1,17 +1,15 @@ class Sessions::Event::Maintenance < Sessions::Event::Base + database_connection_required - def initialize(params) - super(params) - return if !@is_web_socket +=begin - ActiveRecord::Base.establish_connection - end +Event module to broadcast maintenance messages to all client connections. - def destroy - return if !@is_web_socket +To execute this manually, just paste the following into the browser console - ActiveRecord::Base.remove_connection - end + App.WebSocket.send({event:'maintenance', data: {some: 'key'}}) + +=end def run diff --git a/lib/sessions/event/ping.rb b/lib/sessions/event/ping.rb index e4de5cc2b..4524db52f 100644 --- a/lib/sessions/event/ping.rb +++ b/lib/sessions/event/ping.rb @@ -1,5 +1,15 @@ 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 { event: 'pong', diff --git a/lib/sessions/event/spool.rb b/lib/sessions/event/spool.rb index c47ea3f2e..4da08cecb 100644 --- a/lib/sessions/event/spool.rb +++ b/lib/sessions/event/spool.rb @@ -1,6 +1,15 @@ 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 # error handling @@ -11,8 +20,13 @@ class Sessions::Event::Spool < Sessions::Event::Base end if !@session || !@session['id'] - log 'error', "can't send spool, session not authenticated" - return + log 'error', "Can't send spool, session not authenticated" + return { + event: 'error', + data: { + error: 'Can\'t send spool, session not authenticated', + }, + } end spool = Sessions.spool_list(@payload['timestamp'], @session['id']) diff --git a/lib/sessions/event/ticket_overview_index.rb b/lib/sessions/event/ticket_overview_index.rb index e952fddf4..1fda73cbd 100644 --- a/lib/sessions/event/ticket_overview_index.rb +++ b/lib/sessions/event/ticket_overview_index.rb @@ -1,4 +1,5 @@ class Sessions::Event::TicketOverviewIndex < Sessions::Event::Base + database_connection_required def run return if !valid_session? diff --git a/lib/sessions/event/ticket_overview_list.rb b/lib/sessions/event/ticket_overview_list.rb index a5102d172..99c89ef5e 100644 --- a/lib/sessions/event/ticket_overview_list.rb +++ b/lib/sessions/event/ticket_overview_list.rb @@ -1,4 +1,5 @@ class Sessions::Event::TicketOverviewList < Sessions::Event::Base + database_connection_required def run return if !valid_session? diff --git a/lib/sessions/event/who_am_i.rb b/lib/sessions/event/who_am_i.rb new file mode 100644 index 000000000..5f1a40f30 --- /dev/null +++ b/lib/sessions/event/who_am_i.rb @@ -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 diff --git a/spec/requests/long_polling_spec.rb b/spec/requests/long_polling_spec.rb new file mode 100644 index 000000000..d922aacb7 --- /dev/null +++ b/spec/requests/long_polling_spec.rb @@ -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