diff options
Diffstat (limited to 'library/ajaxchat/chat/socket/server.rb')
-rw-r--r-- | library/ajaxchat/chat/socket/server.rb | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/library/ajaxchat/chat/socket/server.rb b/library/ajaxchat/chat/socket/server.rb new file mode 100644 index 000000000..c2f532c84 --- /dev/null +++ b/library/ajaxchat/chat/socket/server.rb @@ -0,0 +1,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])
\ No newline at end of file |