Feature: Chat origin whitelisting functionality.

This commit is contained in:
Rolf Schmidt 2020-02-13 09:27:36 +01:00 committed by Thorsten Eckel
parent eb830a2e15
commit 33498bac91
10 changed files with 96 additions and 39 deletions

View file

@ -1,5 +1,5 @@
class App.Chat extends App.Model class App.Chat extends App.Model
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'block_country', 'note' @configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'whitelisted_websites', 'block_country', 'note'
@extend Spine.Model.Ajax @extend Spine.Model.Ajax
@url: @apiPath + '/chats' @url: @apiPath + '/chats'
@countries: @countries:
@ -250,6 +250,7 @@ class App.Chat extends App.Model
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, { name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 }, { name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
{ name: 'block_ip', display: 'Blocked IPs (separated by ;)', tag: 'input', default: '', null: true }, { name: 'block_ip', display: 'Blocked IPs (separated by ;)', tag: 'input', default: '', null: true },
{ name: 'whitelisted_websites', display: 'Allow websites (separated by ;)', tag: 'input', default: '', null: true },
{ name: 'block_country', display: 'Blocked countries', tag: 'column_select', multiple: true, null: true, default: '', options: @countries, seperator: ';' }, { name: 'block_country', display: 'Blocked countries', tag: 'column_select', multiple: true, null: true, default: '', options: @countries, seperator: ';' },
{ name: 'active', display: 'Active', tag: 'active', default: true }, { name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }, { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },

View file

@ -617,6 +617,23 @@ check if ip address is blocked for chat
=begin =begin
check if website is allowed for chat
chat = Chat.find(123)
chat.website_whitelisted?('zammad.org')
=end
def website_whitelisted?(website)
return true if whitelisted_websites.blank?
whitelisted_websites.split(';').any? do |whitelisted_website|
website.downcase.include?(whitelisted_website.downcase.strip)
end
end
=begin
check if country is blocked for chat check if country is blocked for chat
chat = Chat.find(123) chat = Chat.find(123)
@ -633,10 +650,9 @@ check if country is blocked for chat
return false if geo_ip['country_code'].blank? return false if geo_ip['country_code'].blank?
countries = block_country.split(';') countries = block_country.split(';')
countries.each do |local_country| countries.any? do |local_country|
return true if geo_ip['country_code'] == local_country geo_ip['country_code'] == local_country
end end
false
end end
end end

View file

@ -483,6 +483,7 @@ class CreateTicket < ActiveRecord::Migration[4.2]
t.boolean :public, null: false, default: false t.boolean :public, null: false, default: false
t.string :block_ip, limit: 5000, null: true t.string :block_ip, limit: 5000, null: true
t.string :block_country, limit: 5000, null: true t.string :block_country, limit: 5000, null: true
t.string :whitelisted_websites, limit: 5000, null: true
t.string :preferences, limit: 5000, null: true t.string :preferences, limit: 5000, null: true
t.integer :updated_by_id, null: false t.integer :updated_by_id, null: false
t.integer :created_by_id, null: false t.integer :created_by_id, null: false

View file

@ -0,0 +1,9 @@
class ChatAddAllowWebsite < ActiveRecord::Migration[5.1]
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
add_column :chats, :whitelisted_websites, :string, limit: 5000, null: true
end
end

View file

@ -105,6 +105,14 @@ class Sessions::Event::Base
user user
end end
def remote_ip
@headers&.fetch('X-Forwarded-For', nil).presence
end
def origin
@headers&.fetch('Origin', nil).presence
end
def permission_check(key, event) def permission_check(key, event)
user = current_user user = current_user
return if !user return if !user

View file

@ -24,17 +24,17 @@ return is sent as message back to peer
# geo ip lookup # geo ip lookup
geo_ip = nil geo_ip = nil
if @remote_ip if remote_ip
geo_ip = Service::GeoIp.location(@remote_ip) geo_ip = Service::GeoIp.location(remote_ip)
end end
# dns lookup # dns lookup
dns_name = nil dns_name = nil
if @remote_ip if remote_ip
begin begin
dns = Resolv::DNS.new dns = Resolv::DNS.new
dns.timeouts = 3 dns.timeouts = 3
result = dns.getname @remote_ip result = dns.getname remote_ip
if result if result
dns_name = result.to_s dns_name = result.to_s
end end
@ -51,7 +51,7 @@ return is sent as message back to peer
preferences: { preferences: {
url: @payload['data']['url'], url: @payload['data']['url'],
participants: [@client_id], participants: [@client_id],
remote_ip: @remote_ip, remote_ip: remote_ip,
geo_ip: geo_ip, geo_ip: geo_ip,
dns_name: dns_name, dns_name: dns_name,
}, },

View file

@ -21,8 +21,9 @@ return is sent as message back to peer
def run def run
return super if super return super if super
return if !check_chat_exists return if !check_chat_exists
return if !check_chat_block_by_ip return if blocked_ip?
return if !check_chat_block_by_country return if blocked_country?
return if blocked_origin?
# check if it's a chat sessin reconnect # check if it's a chat sessin reconnect
session_id = nil session_id = nil
@ -54,10 +55,28 @@ return is sent as message back to peer
} }
end end
def check_chat_block_by_ip def blocked_ip?
chat = current_chat return false if !current_chat.blocked_ip?(remote_ip)
return true if !chat.blocked_ip?(@remote_ip)
send_unavailable
true
end
def blocked_country?
return false if !current_chat.blocked_country?(remote_ip)
send_unavailable
true
end
def blocked_origin?
return false if current_chat.website_whitelisted?(origin)
send_unavailable
true
end
def send_unavailable
error = { error = {
event: 'chat_error', event: 'chat_error',
data: { data: {
@ -65,21 +84,5 @@ return is sent as message back to peer
}, },
} }
Sessions.send(@client_id, error) Sessions.send(@client_id, error)
false
end end
def check_chat_block_by_country
chat = current_chat
return true if !chat.blocked_country?(@remote_ip)
error = {
event: 'chat_error',
data: {
state: 'chat_unavailable',
},
}
Sessions.send(@client_id, error)
false
end
end end

View file

@ -48,7 +48,6 @@ class WebsocketServer
def self.onopen(websocket, handshake) def self.onopen(websocket, handshake)
headers = handshake.headers headers = handshake.headers
remote_ip = get_remote_ip(headers)
client_id = websocket.object_id.to_s client_id = websocket.object_id.to_s
log 'notice', 'Client connected.', client_id log 'notice', 'Client connected.', client_id
Sessions.create( client_id, {}, { type: 'websocket' } ) Sessions.create( client_id, {}, { type: 'websocket' } )
@ -60,7 +59,6 @@ class WebsocketServer
last_ping: Time.now.utc.to_i, last_ping: Time.now.utc.to_i,
error_count: 0, error_count: 0,
headers: headers, headers: headers,
remote_ip: remote_ip,
} }
end end
@ -103,7 +101,7 @@ class WebsocketServer
event: data['event'], event: data['event'],
payload: data, payload: data,
session: @clients[client_id][:session], session: @clients[client_id][:session],
remote_ip: @clients[client_id][:remote_ip], headers: @clients[client_id][:headers],
client_id: client_id, client_id: client_id,
clients: @clients, clients: @clients,
options: @options, options: @options,
@ -116,12 +114,6 @@ class WebsocketServer
end end
end end
def self.get_remote_ip(headers)
return headers['X-Forwarded-For'] if headers && headers['X-Forwarded-For']
nil
end
def self.websocket_send(client_id, data) def self.websocket_send(client_id, data)
msg = if data.class != Array msg = if data.class != Array
"[#{data.to_json}]" "[#{data.to_json}]"

9
spec/factories/chat.rb Normal file
View file

@ -0,0 +1,9 @@
FactoryBot.define do
factory :chat do
sequence(:name) { |n| "Chat #{n}" }
max_queue { 5 }
active { true }
created_by_id { 1 }
updated_by_id { 1 }
end
end

18
spec/models/chat_spec.rb Normal file
View file

@ -0,0 +1,18 @@
require 'rails_helper'
RSpec.describe Chat, type: :model do
describe 'website whitelisting' do
let(:chat) { create(:chat, whitelisted_websites: 'zammad.org') }
it 'detects whitelisted website' do
result = chat.website_whitelisted?('https://www.zammad.org')
expect(result).to be true
end
it 'detects non-whitelisted website' do
result = chat.website_whitelisted?('https://www.zammad.com')
expect(result).to be false
end
end
end