diff --git a/src/commands/imc2.py b/src/commands/imc2.py index 5ec7156f1..787fb1ef3 100644 --- a/src/commands/imc2.py +++ b/src/commands/imc2.py @@ -1,7 +1,7 @@ """ IMC2 user and administrative commands. """ -import time +from time import time from django.conf import settings from src.config.models import ConfigValue from src.objects.models import Object @@ -11,25 +11,67 @@ from src.util import functions_general from src.cmdtable import GLOBAL_CMD_TABLE from src.imc2 import connection as imc2_conn from src.imc2.packets import * +from src.imc2.trackers import IMC2_MUDLIST -def cmd_imctest(command): +def cmd_imcwhois(command): """ Shows a player's inventory. """ source_object = command.source_object - source_object.emit_to("Sending") - packet = IMC2PacketWhois(source_object, 'Cratylus') - imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(packet) - source_object.emit_to("Sent") -GLOBAL_CMD_TABLE.add_command("imctest", cmd_imctest) + if not command.command_argument: + source_object.emit_to("Get what?") + return + else: + source_object.emit_to("Sending IMC whois request. If you receive no response, no matches were found.") + packet = IMC2PacketWhois(source_object, command.command_argument) + imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(packet) +GLOBAL_CMD_TABLE.add_command("imcwhois", cmd_imcwhois) def cmd_imckeepalive(command): """ - Shows a player's inventory. + Sends an is-alive packet to the network. """ source_object = command.source_object source_object.emit_to("Sending") packet = IMC2PacketIsAlive() imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(packet) source_object.emit_to("Sent") -GLOBAL_CMD_TABLE.add_command("imckeepalive", cmd_imckeepalive) \ No newline at end of file +GLOBAL_CMD_TABLE.add_command("imckeepalive", cmd_imckeepalive) + +def cmd_imckeeprequest(command): + """ + Sends a keepalive-request packet to the network. + """ + source_object = command.source_object + source_object.emit_to("Sending") + packet = IMC2PacketKeepAliveRequest() + imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(packet) + source_object.emit_to("Sent") +GLOBAL_CMD_TABLE.add_command("imckeeprequest", cmd_imckeeprequest) + +def cmd_imclist(command): + """ + Shows the list of cached games from the IMC2 Mud list. + """ + source_object = command.source_object + + retval = 'Active MUDs on %s\n\r' % imc2_conn.IMC2_PROTOCOL_INSTANCE.network_name + for name, mudinfo in IMC2_MUDLIST.mud_list.items(): + mudline = ' %-20s %s' % (name, mudinfo.versionid) + retval += '%s\n\r' % mudline[:78] + retval += '%s active MUDs found.' % len(IMC2_MUDLIST.mud_list) + source_object.emit_to(retval) +GLOBAL_CMD_TABLE.add_command("imclist", cmd_imclist) + +def cmd_imclistupdated(command): + """ + Shows the list of cached games from the IMC2 Mud list. + """ + source_object = command.source_object + + retval = 'Active MUDs on %s\n\r' % imc2_conn.IMC2_PROTOCOL_INSTANCE.network_name + for name, mudinfo in IMC2_MUDLIST.mud_list.items(): + tdelta = time() - mudinfo.last_updated + retval += ' %-20s %s\n\r' % (name, tdelta) + source_object.emit_to(retval) +GLOBAL_CMD_TABLE.add_command("imclistupdated", cmd_imclistupdated) \ No newline at end of file diff --git a/src/events.py b/src/events.py index c91689bc0..601f7d9ac 100644 --- a/src/events.py +++ b/src/events.py @@ -14,16 +14,7 @@ class IntervalEvent(object): """ Represents an event that is triggered periodically. Sub-class this and fill in the stub function. - """ - # This is what shows up on @ps in-game. - name = None - # An interval (in seconds) for execution. - interval = None - # A timestamp (int) for the last time the event was fired. - time_last_executed = None - # A reference to the task.LoopingCall object. - looped_task = None - + """ def __init__(self): """ Executed when the class is instantiated. @@ -31,6 +22,12 @@ class IntervalEvent(object): # This is set to prevent a Nonetype exception on @ps before the # event is fired for the first time. self.time_last_executed = time.time() + # This is what shows up on @ps in-game. + self.name = None + # An interval (in seconds) for execution. + self.interval = None + # A reference to the task.LoopingCall object. + self.looped_task = None def __unicode__(self): """ @@ -78,9 +75,11 @@ class IEvt_Check_Sessions(IntervalEvent): """ Event: Check all of the connected sessions. """ - name = 'IEvt_Check_Sessions' - interval = 60 - description = "Session consistency checks." + def __init__(self): + super(IEvt_Check_Sessions, self).__init__() + self.name = 'IEvt_Check_Sessions' + self.interval = 60 + self.description = "Session consistency checks." def event_function(self): """ diff --git a/src/imc2/connection.py b/src/imc2/connection.py index f0609807b..2eeef00fc 100644 --- a/src/imc2/connection.py +++ b/src/imc2/connection.py @@ -2,14 +2,16 @@ IMC2 client module. Handles connecting to and communicating with an IMC2 server. """ import telnetlib -import time +from time import time from twisted.internet.protocol import ClientFactory from twisted.protocols.basic import LineReceiver from twisted.internet import reactor, task from twisted.conch.telnet import StatefulTelnetProtocol from django.conf import settings -from src.imc2.packets import * from src import logger +from src.imc2.packets import * +from src.imc2.trackers import * +from src.imc2 import reply_listener # The active instance of IMC2Protocol. Set at server startup. IMC2_PROTOCOL_INSTANCE = None @@ -20,8 +22,9 @@ class IMC2Protocol(StatefulTelnetProtocol): authentication, and all necessary packets. """ def __init__(self): - print "IMC2: Client connecting to %s:%s..." % (settings.IMC2_SERVER_ADDRESS, - settings.IMC2_SERVER_PORT) + logger.log_infomsg("IMC2: Client connecting to %s:%s..." % ( + settings.IMC2_SERVER_ADDRESS, + settings.IMC2_SERVER_PORT)) global IMC2_PROTOCOL_INSTANCE IMC2_PROTOCOL_INSTANCE = self self.is_authenticated = False @@ -34,9 +37,9 @@ class IMC2Protocol(StatefulTelnetProtocol): """ Triggered after connecting to the IMC2 network. """ - print "IMC2: Connected to network server." + logger.log_infomsg("IMC2: Connected to network server.") self.auth_type = "plaintext" - print "IMC2: Sending authentication packet." + logger.log_infomsg("IMC2: Sending authentication packet.") self.send_packet(IMC2PacketAuthPlaintext()) def send_packet(self, packet): @@ -50,7 +53,7 @@ class IMC2Protocol(StatefulTelnetProtocol): packet.imc2_protocol = self packet_str = packet.assemble() - print "IMC2: SENT> %s" % packet_str + logger.log_infomsg("IMC2: SENT> %s" % packet_str) self.sendLine(packet_str) def _parse_auth_response(self, line): @@ -66,8 +69,11 @@ class IMC2Protocol(StatefulTelnetProtocol): self.server_name = line_split[1] self.network_name = line_split[4] self.is_authenticated = True - self.sequence = int(time.time()) - print "IMC2: Successfully authenticated to the '%s' network." % self.network_name + self.sequence = int(time()) + logger.log_infomsg("IMC2: Successfully authenticated to the '%s' network." % self.network_name) + # Let everyone know we've arrived. + #self.send_packet(IMC2PacketKeepAliveRequest()) + self.send_packet(IMC2PacketIsAlive()) def lineReceived(self, line): """ @@ -78,8 +84,14 @@ class IMC2Protocol(StatefulTelnetProtocol): self._parse_auth_response(line) else: logger.log_infomsg("PACKET: %s" % line) - logger.log_infomsg(IMC2Packet(packet_str = line)) - #print "receive:", line + packet = IMC2Packet(packet_str = line) + logger.log_infomsg(packet) + if packet.packet_type == 'is-alive': + IMC2_MUDLIST.update_mud_from_packet(packet) + elif packet.packet_type == 'whois-reply': + reply_listener.handle_whois_reply(packet) + elif packet.packet_type == 'close-notify': + IMC2_MUDLIST.remove_mud_from_packet(packet) class IMC2ClientFactory(ClientFactory): """ diff --git a/src/imc2/events.py b/src/imc2/events.py index affd8aaa4..9590a778b 100644 --- a/src/imc2/events.py +++ b/src/imc2/events.py @@ -3,30 +3,78 @@ This module contains all IMC2 events that are triggered periodically. Most of these are used to maintain the existing connection and keep various lists/caches up to date. """ +from time import time from src import events from src import scheduler from src.imc2 import connection as imc2_conn from src.imc2.packets import * +from src.imc2.trackers import IMC2_MUDLIST -class IEvt_IMC2_IsAlive(events.IntervalEvent): +class IEvt_IMC2_Send_IsAlive(events.IntervalEvent): """ Event: Send periodic keepalives to network neighbors. This lets the other games know that our game is still up and connected to the network. Also provides some useful information about the client game. """ - name = 'IEvt_IMC2_IsAlive' - # Send keep-alive packets every 15 minutes. - interval = 900 - description = "Send an IMC2 is-alive packet." + def __init__(self): + super(IEvt_IMC2_Send_IsAlive, self).__init__() + self.name = 'IEvt_IMC2_Send_IsAlive' + # Send keep-alive packets every 15 minutes. + self.interval = 900 + self.description = "Send an IMC2 is-alive packet." def event_function(self): """ This is the function that is fired every self.interval seconds. """ imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(IMC2PacketIsAlive()) + +class IEvt_IMC2_Send_Keepalive_Request(events.IntervalEvent): + """ + Event: Sends a keepalive-request to connected games in order to see who + is connected. + """ + def __init__(self): + super(IEvt_IMC2_Send_Keepalive_Request, self).__init__() + self.name = 'IEvt_IMC2_Send_Keepalive_Request' + self.interval = 3500 + self.description = "Send an IMC2 keepalive-request packet." + + def event_function(self): + """ + This is the function that is fired every self.interval seconds. + """ + imc2_conn.IMC2_PROTOCOL_INSTANCE.send_packet(IMC2PacketKeepAliveRequest()) + +class IEvt_IMC2_Prune_Inactive_Muds(events.IntervalEvent): + """ + Event: Prunes games that have not sent is-alive packets for a while. If + we haven't heard from them, they're probably not connected or don't + implement the protocol correctly. In either case, good riddance to them. + """ + def __init__(self): + super(IEvt_IMC2_Prune_Inactive_Muds, self).__init__() + self.name = 'IEvt_IMC2_Prune_Inactive_Muds' + # Check every 30 minutes. + self.interval = 1800 + self.description = "Check IMC2 list for inactive games." + # Threshold for game inactivity (in seconds). + self.inactive_thresh = 3599 + + def event_function(self): + """ + This is the function that is fired every self.interval seconds. + """ + for name, mudinfo in IMC2_MUDLIST.mud_list.items(): + # If we haven't heard from the game within our threshold time, + # we assume that they're dead. + if time() - mudinfo.last_updated > self.inactive_thresh: + del IMC2_MUDLIST.mud_list[name] def add_events(): """ Adds the IMC2 events to the scheduler. """ - scheduler.add_event(IEvt_IMC2_IsAlive()) \ No newline at end of file + scheduler.add_event(IEvt_IMC2_Send_IsAlive()) + scheduler.add_event(IEvt_IMC2_Prune_Inactive_Muds()) + scheduler.add_event(IEvt_IMC2_Send_Keepalive_Request()) \ No newline at end of file diff --git a/src/imc2/packets.py b/src/imc2/packets.py index f4b45c40b..fade92047 100644 --- a/src/imc2/packets.py +++ b/src/imc2/packets.py @@ -40,7 +40,7 @@ class IMC2Packet(object): self.target = None self.destination = None # Optional data. - self.optional_data = [] + self.optional_data = {} # Reference to the IMC2Protocol object doing the sending. self.imc2_protocol = None @@ -56,7 +56,6 @@ class IMC2Packet(object): if counter == 0: # This is the sender@origin token. sender_origin = token - print token split_sender_origin = sender_origin.split('@') self.sender = split_sender_origin[0].strip() self.origin = split_sender_origin[1] @@ -84,8 +83,12 @@ class IMC2Packet(object): self.destination = split_target_destination[0] elif counter > 4: # Populate optional data. - key, value = token.split('=', 1) - self.optional_data.append((key, value)) + try: + key, value = token.split('=', 1) + self.optional_data[key] = value + except ValueError: + # Failed to split on equal sign, disregard. + pass # Increment and continue to the next token (if applicable) counter += 1 @@ -129,6 +132,8 @@ class IMC2Packet(object): if self.sender == '*': # Some packets have no sender. return '*' + elif str(self.sender).isdigit(): + return self.sender elif self.sender: # Player object. name = self.sender.get_name(fullname=False, show_dbref=False, @@ -199,7 +204,12 @@ class IMC2PacketKeepAliveRequest(IMC2Packet): Example of a sent keepalive-request: *@YourMUD 1234567890 YourMUD keepalive-request *@* """ - pass + def __init__(self): + super(IMC2PacketKeepAliveRequest, self).__init__() + self.sender = '*' + self.packet_type = 'keepalive-request' + self.target = '*' + self.destination = '*' class IMC2PacketIsAlive(IMC2Packet): """ @@ -607,7 +617,8 @@ class IMC2PacketWhois(IMC2Packet): """ def __init__(self, pobject, whois_target): super(IMC2PacketWhois, self).__init__() - self.sender = pobject + # Use the dbref, it's easier to trace back for the whois-reply. + self.sender = pobject.id self.packet_type = 'whois' self.target = whois_target self.destination = '*' diff --git a/src/imc2/reply_listener.py b/src/imc2/reply_listener.py new file mode 100644 index 000000000..b3352ce2f --- /dev/null +++ b/src/imc2/reply_listener.py @@ -0,0 +1,12 @@ +""" +This module handles some of the -reply packets like whois-reply. +""" +from src.objects.models import Object + +def handle_whois_reply(packet): + try: + pobject = Object.objects.get(id=packet.target) + pobject.emit_to('Whois reply: %s' % packet.optional_data.get('text', 'Unknown')) + except Object.DoesNotExist: + # No match found for whois sender. Ignore it. + pass \ No newline at end of file diff --git a/src/imc2/trackers.py b/src/imc2/trackers.py new file mode 100644 index 000000000..5899c248e --- /dev/null +++ b/src/imc2/trackers.py @@ -0,0 +1,44 @@ +""" +Certain periodic packets are sent by connected MUDs (is-alive, user-cache, +etc). The IMC2 protocol assumes that each connected MUD will capture these and +populate/maintain their own lists of other servers connected. This module +contains stuff like this. +""" +from time import time + +class IMC2Mud(object): + """ + Stores information about other games connected to our current IMC2 network. + """ + def __init__(self, packet): + self.name = packet.origin + self.versionid = packet.optional_data.get('versionid', None) + self.networkname = packet.optional_data.get('networkname', None) + self.url = packet.optional_data.get('url', None) + self.host = packet.optional_data.get('host', None) + self.port = packet.optional_data.get('port', None) + self.sha256 = packet.optional_data.get('sha256', None) + # This is used to determine when a Mud has fallen into inactive status. + self.last_updated = time() + +class IMC2MudList(object): + """ + Keeps track of other MUDs connected to the IMC network. + """ + def __init__(self): + # Mud list is stored in a dict, key being the IMC Mud name. + self.mud_list = {} + + def update_mud_from_packet(self, packet): + # This grabs relevant info from the packet and stuffs it in the + # Mud list for later retrieval. + mud = IMC2Mud(packet) + self.mud_list[mud.name] = mud + + def remove_mud_from_packet(self, packet): + # Removes a mud from the Mud list when given a packet. + mud = IMC2Mud(packet) + del self.mud_list[mud.name] + +# Use this instance to keep track of the other games on the network. +IMC2_MUDLIST = IMC2MudList() \ No newline at end of file