aboutsummaryrefslogtreecommitdiffstats
path: root/library/ajaxchat/chat/socket/server.rb
blob: c2f532c84ad441b8748677933e244774f6451d49 (plain) (blame)
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])