1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
|
#!/usr/bin/env ruby
# Simple Ruby XML Socket Server
#
# This is a a simple socket server implementation in ruby
# to communicate with flash clients via Flash XML Sockets.
#
# The socket code is based on the tutorial
# "Sockets programming in Ruby"
# by M. Tim Jones (mtj@mtjones.com).
#
# Date:: Tue, 05 Mar 2008
# Author:: Sebastian Tschan, https://blueimp.net
# License:: GNU Affero General Public License
# Include socket library:
require 'socket'
# Include XML libraries:
require 'rexml/document'
require 'rexml/streamlistener'
# XML Stream Handler class used to parse chat messages:
class XMLStreamHandler
attr_reader :type,:chat_id,:user_id,:reg_id,:channel_id,:channel_ids
# Called when an opening tag (including attributes) is parsed:
def tag_start name, attrs
case name
when 'root'
# root messages are broadcast messages:
@type = :message
@chat_id = attrs['chatID']
@channel_id = attrs['channelID']
throw :break
when 'register'
# register messages are sent by chat clients:
@type = :register
@chat_id = attrs['chatID']
@user_id = attrs['userID']
@reg_id = attrs['regID']
throw :break
when 'authenticate'
# authenticate messages are sent by the chat server client:
@type = :authenticate
@chat_id = attrs['chatID']
@user_id = attrs['userID']
@reg_id = attrs['regID']
@channel_ids = Array::new
when 'channel'
# authenticate messages contain channel tags:
if @channel_ids
@channel_ids.push(attrs['id'])
else
throw :break
end
when 'policy-file-request'
# policy-file-requests are sent by flash clients for cross-domain authentication:
@type = :policy_file_request
throw :break
else
throw :break
end
end
# Called when a closing tag is parsed:
def tag_end name
if name == 'authenticate'
throw :break
end
end
def text text
# Called on text between tags
end
# Called when cdata is parsed:
alias cdata text
end
# Socket Server class:
class SocketServer
def initialize(config_file)
# List of configuration settings:
@config = Hash::new
# Initialize default settings:
initialize_default_properties
if config_file
# Load settings from configuration file:
load_properties_from_file(config_file)
end
# Sockets list:
@sockets = Array::new
# Clients list:
@clients = Hash::new
# Chats list, used to distinguish between different chat installations (contains channels list):
@chats = Hash::new
# Initialize server socket:
initialize_server_socket
if @server_socket
# Log server start (STDOUT.flush prevents output buffering):
puts "#{Time.now}\tServer started on Port #{@config[:server_port].to_s} ..."; STDOUT.flush
begin
# Start the server:
run
rescue SignalException
# Controlled stop:
ensure
for socket in @sockets
if socket != @server_socket
# Disconnect all clients:
handle_client_disconnection(socket, false)
end
end
@sockets = nil
@clients = nil
# Log server stop:
puts "#{Time.now}\tServer stopped."; STDOUT.flush
end
end
end
def run
# Endless loop:
while 1
# Blocking select call. The first three parameters are arrays of IO objects or nil.
# The last parameter is to set a timeout in seconds to force select to return
# if no event has occurred on any of the given IO object arrays.
res = select(@sockets, nil, nil, nil)
if res != nil then
# Iterate through the tagged read descriptors:
for socket in res[0]
# Received a connect to the server socket:
if socket == @server_socket then
accept_new_connection
else
# Received something on a client socket:
if socket.eof? then
# Handle client disconnection:
handle_client_disconnection(socket)
else
# Handle client input data:
handle_client_input(socket, socket.gets(@config[:eol]))
end
end
end
end
end
end
private
def initialize_default_properties
# Server address (empty = bind to all available interfaces):
@config[:server_address] = ''
# Server port:
@config[:server_port] = 1935
# Comma-separated list of clients allowed to broadcast (allows all if empty):
@config[:broadcast_clients] = ''
# Defines if broadcast is sent to broadcasting client:
@config[:broadcast_self] = false
# Maximum number of clients (0 allows an unlimited number of clients):
@config[:max_clients] = 0
# Comma-separated list of domains from which downloaded Flash clients are allowed to connect (* allows all domains):
@config[:allow_access_from] = '*'
# Defines the cross-domain-policy string sent to Flash clients as response to a policy-file-request:
@config[:cross_domain_policy] = '<cross-domain-policy><allow-access-from domain="'+@config[:allow_access_from]+'" to-ports="'+@config[:server_port].to_s+'"/></cross-domain-policy>'
# EOL (End Of Line) character used by Flash XML Socket communication (a null-byte):
@config[:eol] = "\0"
# Log level (0 logs only errors and server start/stop, 1 logs client connections, 2 logs all messages but no broadcast content, 3 logs everything):
@config[:log_level] = 0
end
def load_properties_from_file(config_file)
# Open the config file and go through each line:
File.open(config_file, 'r') do |file|
file.read.each_line do |line|
# Remove trailing whitespace from the line:
line.strip!
# Get the position of the first "=":
i = line.index('=')
# Check if line is not a comment and a valid property:
if (!line.empty? && line[0] != ?# && i > 0)
# Add the configuration option to the config hash:
key = line[0..i - 1].strip
value = line[i + 1..-1].strip
# Parse boolean values:
if value.eql?('false')
@config[key.to_sym] = false
elsif value.eql?('true')
@config[key.to_sym] = true
# Parse integer numbers:
elsif value.to_i.to_s.eql?(value)
@config[key.to_sym] = value.to_i
# Parse floating point numbers:
elsif value.to_f.to_s.eql?(value)
@config[key.to_sym] = value.to_f
# Parse string values:
else
@config[key.to_sym] = value
end
end
end
end
if @config[:eol].empty?
# Use default EOL if configuration option is empty:
@config[:eol] = $/
end
end
def initialize_server_socket
begin
# The server socket, allowing connections from any interface and bound to the given port number:
@server_socket = TCPServer.new(@config[:server_address], @config[:server_port].to_i)
# Enable reuse of the server address (e.g. for rapid restarts of the server):
@server_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
# Add the server socket to the sockets list:
@sockets.push(@server_socket)
rescue Exception => error
# Log initialization failure:
puts "#{Time.now}\tFailed to initialize Server on Port #{@config[:server_port].to_s}: #{error}."; STDOUT.flush
end
end
def accept_new_connection
begin
# Accept the client connection (non-blocking):
socket = @server_socket.accept_nonblock
# Retrieve IP and Port:
ip = socket.peeraddr[3]
port = socket.peeraddr[1]
# Check if we have reached the maximum number of connected clients (always accept the broadcast clients):
if @config[:max_clients].to_i == 0 || @clients.size < @config[:max_clients].to_i || !@config[:broadcast_clients].empty? && @config[:broadcast_clients].include?(ip)
# Add the accepted socket connection to the socket list:
@sockets.push(socket)
# Create a new Hash to store the client data:
client = Hash::new
client[:id] = "[#{ip}]:#{port}"
# Check if the client is allowed to broadcast:
if @config[:broadcast_clients].empty? || @config[:broadcast_clients].include?(ip)
client[:allowed_to_broadcast] = true
else
client[:allowed_to_broadcast] = false
end
# Add the client to the clients list:
@clients[socket] = client
if @config[:log_level].to_i > 0
# Log client connection and the number of connected clients:
puts "#{Time.now}\t#{client[:id]} Connects\t(#{@clients.size} connected)"; STDOUT.flush
end
else
# Close the socket connection:
socket.close
end
rescue
# Client disconnected before the address information (IP, Port) could be retrieved.
end
end
def handle_client_disconnection(client_socket, delete_socket=true)
# Retrieve the client ID for the current socket:
client_id = @clients[client_socket][:id]
begin
# Close the socket connection:
client_socket.close
rescue
# Rescue if closing the socket fails
end
if delete_socket
# Remove the socket from the sockets list:
@sockets.delete(client_socket)
end
# Remove the client ID from the clients list:
@clients.delete(client_socket)
if @config[:log_level].to_i > 0
# Log client disconnection and the number of connected clients:
puts "#{Time.now}\t#{client_id} Disconnects\t(#{@clients.size} connected)"; STDOUT.flush
end
end
def handle_client_input(client_socket, str)
# Create a new XML stream handler:
handler = XMLStreamHandler.new
begin
# As soon as the parser has found the relevant information it throws a :break symbol:
catch :break do
# Parse the given input string for XML messages:
REXML::Document.parse_stream(str, handler)
end
# The handler stores a type property to define the parsed XML message:
case handler.type
when :message
handle_broadcast_message(client_socket, handler.chat_id, handler.channel_id, str)
when :register
handle_client_registration(client_socket, handler.chat_id, handler.user_id, handler.reg_id)
when :authenticate
handle_client_authentication(client_socket, handler.chat_id, handler.user_id, handler.reg_id, handler.channel_ids)
when :policy_file_request
handle_policy_file_request(client_socket)
end
rescue Exception => error
# Rescue if parsing the client input fails and log the error message:
puts "#{Time.now}\t#{@clients[client_socket][:id]} Client Input Error:#{error.to_s.dump}"; STDOUT.flush
end
end
def handle_broadcast_message(client_socket, chat_id, channel_id, str)
# Check if the_client is allowed to broadcast:
if @clients[client_socket][:allowed_to_broadcast]
# Check if the chat and channel have been registered:
if @chats[chat_id] && (@chats[chat_id][channel_id] || @chats[chat_id]['ALL'])
# Go through the sockets list:
@sockets.each do |socket|
# Skip the server socket and skip the the client socket if broadcast is not to be sent to self:
if socket != @server_socket && (@config[:broadcast_self] || socket != client_socket)
# Only write to clients registered to the given channel or to the "ALL" channel:
if @chats[chat_id]['ALL']
reg_id = @chats[chat_id]['ALL'][@clients[socket][:user_id]]
end
if !reg_id && @chats[chat_id][channel_id]
reg_id = @chats[chat_id][channel_id][@clients[socket][:user_id]]
end
# Check if the reg_id stored for the given channel and user_id matches the clients reg_id:
if reg_id && reg_id.eql?(@clients[socket][:reg_id])
begin
# Write the broadcast message on the socket connection:
socket.write(str)
rescue
# Rescue if writing to the socket fails
end
end
end
end
end
if @config[:log_level].to_i > 2
# Log the message sent by the broadcast client:
puts "#{Time.now}\t#{@clients[client_socket][:id]} Chat:#{chat_id.to_s.dump} Channel:#{channel_id.to_s.dump} Message:#{str.to_s.dump}"; STDOUT.flush
elsif @config[:log_level].to_i > 1
# Log the message sent by the broadcast client:
puts "#{Time.now}\t#{@clients[client_socket][:id]} Chat:#{chat_id.to_s.dump} Channel:#{channel_id.to_s.dump} Message"; STDOUT.flush
end
end
end
def handle_client_registration(client_socket, chat_id, user_id, reg_id)
# Save the chat_id, use_id and reg_id as client properties:
@clients[client_socket][:chat_id] = chat_id
@clients[client_socket][:user_id] = user_id
@clients[client_socket][:reg_id] = reg_id
if @config[:log_level].to_i > 1
# Log the client registration:
puts "#{Time.now}\t#{@clients[client_socket][:id]} Chat:#{chat_id.to_s.dump} User:#{user_id.to_s.dump} Reg:#{reg_id.to_s.dump}"; STDOUT.flush
end
end
def handle_client_authentication(client_socket, chat_id, user_id, reg_id, channel_ids)
# Only the broadcast clients may send authentication messages:
if @clients[client_socket][:allowed_to_broadcast]
# Create a new chat item if not found for the given chat_id:
if !@chats[chat_id]
@chats[chat_id] = Hash.new
end
# Go through the list of channels for the given chat:
@chats[chat_id].each_key do |key|
# Delete all items for the given user on all channels of the given chat:
@chats[chat_id][key].delete(user_id)
# If the chat channel is empty, delete the channel item:
if @chats[chat_id][key].size == 0
@chats[chat_id].delete(key)
end
end
# Go through the list of authenticated channel_ids:
channel_ids.each do |channel_id|
# Create a new channel item if not found for the current channel_id (and the given chat_id):
if !@chats[chat_id][channel_id]
@chats[chat_id][channel_id] = Hash.new
end
# Add a user item of the given user_id with the given reg_id to the current channel:
@chats[chat_id][channel_id][user_id] = reg_id
end
if @config[:log_level].to_i > 1
# Log the client authentication:
puts "#{Time.now}\t#{@clients[client_socket][:id]} Chat:#{chat_id.to_s.dump} User:#{user_id.to_s.dump} Auth:#{reg_id.to_s.dump} Channels:#{channel_ids.join(',').dump}"; STDOUT.flush
end
end
end
def handle_policy_file_request(client_socket)
begin
# Write the cross-domain-policy to the Flash client:
client_socket.write(@config[:cross_domain_policy]+@config[:eol])
rescue
# Rescue if writing to the socket fails
end
if @config[:log_level].to_i > 1
# Log the policy-file-request:
puts "#{Time.now}\t#{@clients[client_socket][:id]} Policy-File-Request"; STDOUT.flush
end
end
end
# Start the socket server with the first command line argument as configuration file:
SocketServer.new($*[0])
|