class CRCServer(object): # Initialization method def __init__(self, options, run_on_localhost=False): # DO NOT EDIT ANYTHING BELOW THIS LINE IN __init__ # ----------------------------------------------------------------------------- # Use this selector object for the assignment. self.sel = selectors.DefaultSelector() # Use this MessageParser object for assignment 2 self.parser = MessageParser() # CRCServer accepts an "options" variable on construction, which is used to pass in # several important values from the test configuration file. This includes the # server's name, the port the server socket should listen on, and a human-readable # description of the server (info) self.servername = options.servername self.port = options.port self.info = options.info # The next three variables store information about what machine this server should # attempt to connect to on initialization. Except for the very first CRCServer in a # network, all CRCServers connect to a pre-existing server on initialization. This # pre-existing server will be the new server's point of access into the wider network # The hostname of the server to connect to on startup (e.g. theshire.nz) self.connect_to_host = options.connect_to_host # The IP address of the server to connect to on startup. # IMPORTANT: You MUST use self.connect_to_host_addr when creating a TCP socket to # connect to this machine. This value is set dynamically based on whether # we are testing on localhost, or running in the wild on real servers self.connect_to_host_addr = options.connect_to_host # The port to connect to on the host server self.connect_to_port = options.connect_to_port # If this server is configured to run on localhost, then self.connect_to_host_addr # will be overwritten with the loopback address self.run_on_localhost = run_on_localhost if self.run_on_localhost: self.connect_to_host_addr = '127.0.0.1' # Store the servers who are directly connected to this server # The list should contain the names of the servers self.adjacent_servers = [] # Store all information about servers in this variable # The key should be the servername, and the variable a ServerData object self.servers_lookuptable = {} # Store the users who are directly connected to this server # The list should contain the nick of the users self.adjacent_users = [] # Store all information about users in this variable # The key should be the user's nick, and the variable a UserData object self.users_lookuptable = {} # Options to help with debugging and logging self.log_file = options.log_file self.logger = None self.init_logging() # THIS WILL BE SET TO TRUE BY CRCTestManager.py WHEN IT IS TIME TO TERMINATE THIS PROCESS # DO NOT CHANGE THIS VALUE IN YOUR CODE self.request_terminate = False # This dictionary contains mappings from commands to command handlers. # Upon receiving a command X, the appropriate command handler can be called with: self.message_handlers[X](...args) self.message_handlers = { # Connection Registration message handlers "USER": self.handle_user_message, "SERVER": self.handle_server_message, "QUIT": self.handle_quit_message, } # This dictionary maps human-readable reply/error messages to their numerical representations. # The numerical representation must be sent to clients, not the human-readable version. # The full format for each reply/error message is included next to each command as a comment self.reply_codes = { "RPL_WELCOME": 1, # :server_name ### :Welcome to the Internet Relay Network <nick>!<user>@<host> "ERR_NOSUCHNICK": 401, # :server_name ### <nick> :No such nick "ERR_NICKCOLLISION": 436, # :server_name ### <nick> :Nickname collision KILL from <user>@<host> "ERR_NEEDMOREPARAMS": 461, # :server_name ### <command> :Not enough parameters } # DO NOT EDIT THIS METHOD # Setup the server and start listening for incoming messages def run(self): self.print_info("Launching server %s..." % self.servername) # Set up the server socket that will listen for new connections self.setup_server_socket() # If we are supposed to connect to another server on startup, then do so now if self.connect_to_host and self.connect_to_port: self.connect_to_server() # Start listening for connections on the server socket self.listen_for_messages() # This function is responsible for setting up the server socket and registering it with your selector # TODO: Create a TCP server socket and bind to self.port (defined in __init__). # Begin listening for incoming connections and register the socket with your selector # HINT: Server sockets are read from, but never written to. This is important when registering the socket # with your selector # HINT: Later on, you will need to differentiate between the server socket (which accepts new connections) # and sockets serving connections with other servers and clients. Select won't tell you which is which, # it just tells you that a socket is ready for processing. When registering the server socket, you can # use the data parameter to make this possible def setup_server_socket(self): Server = socket(AF_INET, SOCK_STREAM) Server.bind(('', self.port)) Server.setblocking(False) Server.listen(1) self.sel = selectors.DefaultSelector() #connectionSocket, addr = sock.accept() #self.prot.setblocking(False) #event read is when the socket wants to read information #event write is when the socket wants to write infromaiton/ out going communication events = selectors.EVENT_READ data = 'M', ConnectionData self.sel.register(Server, events, data) pass # This function is responsible for connecting to a remote CRC server upon starting this server # The details of the server to connect to are set in self.connect_to_host_addr and self.connect_to_port # TODO: Establish a connection with the remote server, register the new socket with your selector, # and send a SERVER registration message to the server you've connected to. # The SERVER registration message should be of the format: # SERVER [servername] [hopcount=1] :[info] # ex. "SERVER lothlorien.nz 1 : Home of the elves" # HINT: This socket will need to be both read from and written to # TODO: You will also need to create an instance of ConnectionData() and assign it # to the data field when registering the connection. ConnectionData is a class created for this assignment. # See the comments at the top of this file for more details. ConnectionData holds our read and write buffers # associated with this socket def connect_to_server(self): connectServer = socket(AF_INET, SOCK_STREAM) connectServer.connect( (self.connect_to_host_addr, self.connect_to_port)) connectServer.setblocking(False) events = selectors.EVENT_READ | selectors.EVENT_WRITE data = 'S', ConnectionData() message = 'SERVER ' + self.servername + ' 1 :' + self.info + '\r\n' data[1].write_buffer = message # defrentiat wetween server socket and client socket # to store data that we need linked to a single socket self.sel.register(connectServer, events, data) pass # This is the main loop responsible for processing input and output on all sockets this server # is connected to. You should manage these connections using the selector self.sel. # TODO: Inside of the while not self.request_terminate loop, get a list of all sockets ready for processing # from your selector, and then process these events. If the socket being processed is the server socket, # call self.accept_new_connection. Otherwise, call self.service_socket. # Once the while loop has terminate (i.e. the program is shutting down), call self.cleanup() # HINT: Pass a short timeout value into your select() call (e.g. 1 second) to prevent your code from hanging # when it is time to terminate it # look at "Enabling non-bloking communication" video at 5:28 def listen_for_messages(self): self.print_info("Listening for new connections on port " + str(self.port)) # All calls to select() MUST be inside of this loop. Select is a blocking call, and we need to terminate the # server in order to test its functionality. We will accomplish this by calling select() inside of a loop that # we can terminate by setting self.request_terminate to True. while not self.request_terminate: events = self.sel.select(timeout=1) for key, mask in events: sock = key.fileobj data = key.data if data[0] == 'M': #use the data to figure out if it was a server or client socket self.accept_new_connection(key) else: self.service_socket(key, mask) # TODO: Implement the above described code within this loop pass self.cleanup() # On shutting down the server, we need to release allocated resources associated with the server socket, with all # other sockets we've opened, and with our selector. Use this function to accomplish this # TODO: Perform any cleanup required upon termination of the program. This includes both sockets and your selector def cleanup(self): sockets = list(self.sel._fd_to_key.values()) for sock in sockets: self.sel.unregister(sock.fileobj) sock.fileobj.close() self.sel.close() pass # This function is responsible for handling new connection requests from other servers and from clients. # NOTE: At this point we don't yet know if the entity who sent this connection request is a server or a client # We won't find this out until we receive either a SERVER or a USER registration message # TODO: Accept the connection request and register it with your selector. You should configure all sockets # for both READ and WRITE events. You will also need to create an instance of ConnectionData() and assign it # to the data field when registering the connection. ConnectionData is a class created for this assignment. # See the comments at the top of this file for more details. ConnectionData holds our read and write buffers # associated with this socket def accept_new_connection(self, select_key): sock = select_key.fileobj connectionSocket, addr = sock.accept() connectionSocket.setblocking(False) events = selectors.EVENT_READ | selectors.EVENT_WRITE data = 'C', ConnectionData() self.sel.register(connectionSocket, events, data) pass # This function is responsible for receiving CRC messages received from connected servers and clients. # TODO: Check to see if this is a READ event and/or a WRITE event (it is possible for it to be both). # If it is a read event, read the data from the connection and process it. If you call recv but # don't receive any data, this means that the client/server has closed their connection from # the other side. In this case, you should unregister and close the socket. # On receiving data, call self.handle_messages and pass in that data # If it is a write event, make sure you actually have data to write before writing to the socket. # You don't want to write empty data to your socket def service_socket(self, select_key, mask): sock = select_key.fileobj data = select_key.data if mask & selectors.EVENT_READ: message = sock.recv(2048) if message: message = message self.handle_messages(select_key, message) data[1].read_buffer = '' else: self.sel.unregister(sock) sock.close() if mask & selectors.EVENT_WRITE: if data[1].write_buffer: sock.send(data[1].write_buffer.encode()) data[1].write_buffer = '' pass # Call this function from server_socket whenever a message is received from a previously connected # client or server and pass in the content of that message. This will get passed to your message parser # and sent on to the appropriate message handler function. def handle_messages(self, select_key, recv_data): messages = self.parser.parse_data(recv_data) for message in messages: # If we recognize the command, then process it using the assigned message handler if message["command"] in self.message_handlers: self.print_info("Received message \"%s\"" % (message)) self.message_handlers[message["command"]](select_key, message["prefix"], message["command"], message["params"]) else: raise Exception("Unrecognized command: " + message["command"]) # very confused about this section??? ###################################################################### # This block of functions should handle all functionality realted to how # the server sends messages. Avoid directly sending messages or responses # in the command handlers, and instead call these functions. This will help with debugging later # This function should implement the functionality used to send a message to another server. # You CANNOT call send() in this function, or in a function directly called by this function. # Remember that send() must be called when handling a selector event with the WRITE mask set to true # Otherwise your code may block and cause your program to hang # TODO: Write the code required when the server has a message to be sent to another server def send_message_to_server(self, name_of_server_to_send_to, message): server = self.servers_lookuptable[name_of_server_to_send_to] server.write_buffer += message pass # When responding to an error, you may not yet know the name of client/server when sent the message # (E.g. when the initial registration command fails.) In this case, you will need to send the message # back using the select_key that was passed into your message handler. The functionality of this code # will be very similar to your send_message_to_server() function, but it will only be called # if you don't know the name of the server/client the message is directed to # TODO: Write the code required when the server has a message to be sent through a select_key def send_message_to_select_key(self, select_key, message): select_key.data[1].write_buffer += message pass # This function should implement the functionality used to send a message to a client. This function # will be slightly different from send_message_to_server(), as messages addressed to clients are first # forwarded to servers, and then sent to the user upon arriving at the server the user is registered to. # You CANNOT call send() in this function, or in a function directly called by this function. # Remember that send() must be called when handling a selector event with the WRITE mask set to true # Otherwise your code may block and cause your program to hang # TODO: Write the code required when the server has a message to be sent to a client def send_message_to_client(self, name_of_client_to_send_to, message): client = self.users_lookuptable[name_of_client_to_send_to] client.write_buffer += message pass # Messages will sometimes need to be sent to every server in the IRC network. This is a helper function # to make that process easier. You may call send_message_to_server() in this function. Make sure you only # send the message to servers that are ADJACENT to this server. # You will sometimes want to exclude a server from receiving this message, such as when forwarding a message # received from another server. In this case, you can't forward this message back to that server or the message # will never die. This is the purpose of the ignore_server parameter. You must NOT broadcast a message # to the server included in that parameter, if it is present (it defaults to None). # TODO: Write the code required to broadcast to all adjacent servers, except for a server included in the # ignore_server parameter def broadcast_message_to_servers(self, message, ignore_server=None): for server in self.servers_lookuptable: if self.servers_lookuptable[server].servername != ignore_server: self.servers_lookuptable[server].write_buffer += message pass def create_numeric_reply(self, reply_key, message): code = self.reply_codes[reply_key] return ":%s %d %s\r\n" % (self.servername, code, message) ###################################################################### # The remaining functions are command handlers. Each command handler is documented with the functionality that # must be supported. Each command handler expects to receive 4 parameters: # * select_key: select_key contains the key value returned by select() for a specific connection. This contains # the socket and the data associated with the socket upon registration with select # * prefix: the prefix of the message to be processed. This should be None if no prefix was present # * command: the command to be processed # * params: a list of the parameters associated with the command. This should be None if no params were present ###################################################################### # Server message # Command: SERVER # Parameters: # <servername>: the name of the new server # <hopcount>: the number of hops required to reach this server # [<info>]: human-readable name for the server # Examples: # SERVER rivendale.irc.edu 1 :The House of Elrond # This is an initial registration command coming from a new server # # that should be connected to this server in the spanning tree # :gondolin.irc.com SERVER rivendale.irc.edu 4 :The House of Elrond # This is a notification from a known server about a new server # # that has connected elsewhere into the spanning tree # Notes: # This function handles the initial registrion process for new servers. The user must provide a unique servername # on registration. Upon receipt of a valid registration method, this function should create a new ServerData object containing # this server's details. This should be stored in the servers_lookuptable, using the server's name as the key associated # with this new value. The server should then notify all other servers about this new server. # # Finally, the server should send the new server all known servers and users. This can be accomplished by sending # SERVER and USER messages, and RPL_TOPIC/RPL_NOTOPIC and RPL_NAMEPLY messages, that inform the new server about every other # known server, user, and channel. Sending SERVER and USER messages will inform the new server about all servers and users # using the normal registration code, and thus requires no additional development. You will need to complete the appropriate # RPL handlers for RPL_TOPIC, RPL_NOTOPIC, and RPL_NAMEPLY to enable the new server to register existing channel information. # These RPL handlers will only be used for this functionality. # # Additionally, the server the new server registers directly with also needs to replace the ConnectionData associated with this socket # that was created in accept_new_connection(). It should replace ConnectionData with the new ServerData object. # The ConnectionData object can be replaced using the selector.modify command (see python docs for more detail). This allows us # to determine that the connection received over that socket is from a server, and to determine which server, for all future # messages received from that socket def handle_server_message(self, select_key, prefix, command, params): noError = True if len(params) < 3: ErrorMessage = '' + command + ' :Not enough parameters' ErrorMessge = self.create_numeric_reply('ERR_NEEDMOREPARAMS', ErrorMessage) noError = False sock = select_key.fileobj data = select_key.data if noError: new_server = ServerData() new_server.servername = params[0] new_server.hopcount = params[1] new_server.info = params[2] self.servers_lookuptable[params[0]] = new_server if prefix == None: self.adjacent_servers.append(params[0]) self.servers_lookuptable[params[0]].first_link = params[0] new_message = ':' + self.servername + ' SERVER ' + self.servername + ' 1' if self.info != None: new_message += ' :' + self.info + '\r\n' else: new_message += '\r\n' events = selectors.EVENT_READ | selectors.EVENT_WRITE new_data = 'S', new_server self.sel.modify(sock, events, new_data) self.send_message_to_server(new_server.servername, new_message) for server in self.servers_lookuptable: if params[0] != self.servers_lookuptable[ server].servername and prefix != self.servers_lookuptable[ server].servername: new_message = ':' + self.servername + ' SERVER ' + self.servers_lookuptable[ server].servername + ' ' + str( int(self.servers_lookuptable[server].hopcount) + 1) if self.servers_lookuptable[server].info != None: new_message += ' :' + self.servers_lookuptable[ server].info + '\r\n' else: new_message += '\r\n' self.send_message_to_server(new_server.servername, new_message) if params[1] == '1' and prefix != None: events = selectors.EVENT_READ | selectors.EVENT_WRITE new_data = 'S', new_server self.sel.modify(sock, events, new_data) self.adjacent_servers.append(params[0]) else: self.servers_lookuptable[params[0]].first_link = prefix hop_count = int(params[1]) + 1 message = ':' + self.servername + ' SERVER ' + params[ 0] + ' ' + str(hop_count) + ' :' + params[2] + '\r\n' if prefix == None: self.broadcast_message_to_servers(message, params[0]) else: self.broadcast_message_to_servers(message, prefix) else: self.send_message_to_select_key(select_key, ErrorMessage) pass ###################################################################### # User message # Command: USER # Parameters: # <nick>: the requested nickname for the new user (nicks may NOT start with '#') # <hostname>: the name of the computer this user is connecting from # <servername>: the name of the server this user is connecting to # [<realname>]: the real name of the user # Examples: # USER samwise bagend theshire.irc.com :Samwise Gamgee # This is an initial registration command coming from a new client # :rivendale.irc.com USER samwise bagend theshire.irc.com :Samwise Gamgee # This is a notification from a server about a new client # Numeric replies: # ERR_NICKCOLLISION: A user with this nick is already registered somewhere on the network # RPL_WELCOME: The registration was successful # Notes: # This function handles the initial registrion process for new users. The user must provide a unique # nick on registration. If this nick is not unique, the function must return a ERR_NICKCOLLISION message. # Upon receipt of a valid registration method, this function should create a new UserData object containing # this user's details. This should be stored in the users_lookuptable, using the user's nick as the key associated # with this new value. Finally, the server should then notify the client that they have registered, using the RPL_WELCOME message, # and should broadcast their message to all other servers to inform them of the user's registration. # # Additionally, the server the user registers directly with also needs to replace the ConnectionData associated with this socket # that was created in accept_new_connection(). It should replace ConnectionData with the new UserData object. # The ConnectionData object can be replaced using the selector.modify command (see python docs for more detail). This allows us # to determine that the connection received over that socket is from a client, and to determine which client, for all future # messages received from that socket def handle_user_message(self, select_key, prefix, command, params): noError = True if len(params) < 3: ErrorMessage = '' + command + ' :Not enough parameters' ErrorMessge = self.create_numeric_reply('ERR_NEEDMOREPARAMS', ErrorMessage) self.send_message_to_select_key(select_key, ErrorMessage) noError = False else: for nick in self.users_lookuptable: if params[0] == nick: noError = False if not noError: if params[0] not in self.users_lookuptable: ErrorMessage = self.create_numeric_reply( 'ERR_NICKCOLLISION', '' + params[0] + ' :Nickname collision KILL from ' + params[3] + '@' + params[1] + '\r\n') self.send_message_to_select_key(select_key, ErrorMessage) if noError: sock = select_key.fileobj data = select_key.data new_user = UserData() new_user.nick = params[0] new_user.hostname = params[1] new_user.servername = params[2] new_user.realname = params[3] self.users_lookuptable[params[0]] = new_user if prefix == None: self.adjacent_users.append(params[0]) self.users_lookuptable[params[0]].first_link = self.servername events = selectors.EVENT_READ | selectors.EVENT_WRITE new_data = 'C', new_user self.sel.modify(sock, events, new_data) User_message = self.create_numeric_reply( 'RPL_WELCOME', ':Welcome to the Internet Relay Network ' + params[0] + '!' + params[3] + '@' + params[1]) self.send_message_to_client(new_user.nick, User_message) else: self.users_lookuptable[params[0]].first_link = prefix self.users_lookuptable[params[0]] = new_user message = ':' + self.servername + ' USER ' + params[ 0] + ' ' + params[1] + ' ' + params[2] + ' :' + params[ 3] + '\r\n' self.broadcast_message_to_servers(message) pass ###################################################################### # Quit message # Command: QUIT # Parameters: # {[<Goodbye message>]}: an optional message from the user who has quit. If no message is provided, # use the default message: <nick> has quit # Examples: # QUIT :shot with an arrow in the chest # A message from a user who is quitting the server # :boromir QUIT :shot with an arrow in the chest # A message from another server about a user who has quit. The user's # # nick is included in the prefix of the message # Numeric replies: # None # Notes: # This function should be called when a user quits the IRC network. All information of this user should be removed from # users_lookuptable and adjacent_users, as well as any channels the user had joined. The Quit message must then be broadcast # to all servers. If the user appended an optional Goodbye message then it should be sent to all users in the channels # the user had joined. def handle_quit_message(self, select_key, prefix, command, params): sock = select_key.fileobj data = select_key.data ignore = None if prefix == None: Q_nick = data[1].nick if Q_nick in self.users_lookuptable: self.users_lookuptable.pop(Q_nick) self.adjacent_users.remove(Q_nick) if prefix != None: Q_nick = prefix if Q_nick in self.users_lookuptable: self.users_lookuptable.pop(Q_nick) message = ':' + Q_nick + ' ' + command + '' if params != None: message += ' :' + params[0] + '\r\n' else: message += ' :' + Q_nick + 'has quit\r\n' if data[0] is 'S': ignore = data[1].servername self.broadcast_message_to_servers(message, ignore) if prefix == None: self.sel.unregister(sock) sock.close() pass # DO NOT EDIT ANY OF THE FUNCTIONS INCLUDED IN IRCServer BELOW THIS LINE # These are helper functions to assist with logging, and list management # ---------------------------------------------------------------------- ###################################################################### # This block of functions enables logging of info, debug, and error messages # Do not edit these functions. init_logging() is already called by the template code # You are encouraged to use print_info, print_debug, and print_error to log # messages useful to you in development def init_logging(self): # If we don't include a log file name, then don't log if not self.log_file: return # Get a reference to the logger for this program self.logger = logging.getLogger("IRCServer") __location__ = os.path.realpath( os.path.join(os.getcwd(), os.path.dirname(__file__))) # Create a file handler to store the log files fh = logging.FileHandler(os.path.join(__location__, 'Logs', '%s' % self.log_file), mode='w') # Set up the logging level. It defaults to INFO log_level = logging.INFO # Define a formatter that will be used to format each line in the log formatter = logging.Formatter(("%(asctime)s - %(name)s[%(process)d] - " "%(levelname)s - %(message)s")) # Assign all of the necessary parameters fh.setLevel(log_level) fh.setFormatter(formatter) self.logger.setLevel(log_level) self.logger.addHandler(fh) def print_info(self, msg): print("[%s] \t%s" % (self.servername, msg)) if self.logger: self.logger.info(msg) # This function takes two lists and returns the union of the lists. If an object appears in both lists, # it will only be in the returned union once. def union(self, lst1, lst2): final_list = list(set(lst1) | set(lst2)) return final_list # This function takes two lists and returns the intersection of the lists. def intersect(self, lst1, lst2): final_list = list(set(lst1) & set(lst2)) return final_list # This function takes two lists and returns the objects that are present in list1 but are NOT # present in list2. This function is NOT commutative def diff(self, list1, list2): return (list(set(list1) - set(list2)))