diff --git a/src/commands/imc2.py b/src/commands/imc2.py new file mode 100644 index 000000000..13a05b0cf --- /dev/null +++ b/src/commands/imc2.py @@ -0,0 +1,24 @@ +""" +IMC2 user and administrative commands. +""" +import time +from django.conf import settings +from src.config.models import ConfigValue +from src.objects.models import Object +from src import defines_global +from src import ansi +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 * + +def cmd_imctest(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) \ No newline at end of file diff --git a/src/config_defaults.py b/src/config_defaults.py index 0ac8e9300..cf75c5f60 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -66,6 +66,35 @@ DATABASE_HOST = '' # Empty string defaults to localhost. Not used with sqlite3. DATABASE_PORT = '' +""" +IMC Configuration + +This is static and important enough to put in the server-side settings file. +Copy and paste this section to your game/settings.py file and change the +values to fit your needs. + +Evennia's IMC2 client was developed against MudByte's network. You may +register and join it by going to: +http://www.mudbytes.net/imc2-intermud-join-network + +Choose "Other unsupported IMC2 version" and enter your information there. +You'll want to change the values below to reflect what you entered. +""" +# Make sure this is True in your settings.py. +IMC2_ENABLED = False +# The hostname/ip address of your IMC2 server of choice. +IMC2_SERVER_ADDRESS = None +# The port to connect to on your IMC2 server. +IMC2_SERVER_PORT = None +# This is your game's IMC2 name. +IMC2_MUDNAME = None +# Your IMC2 client-side password. Used to authenticate with your network. +IMC2_CLIENT_PW = None +# Your IMC2 server-side password. Used to verify your network's identity. +IMC2_SERVER_PW = None +# This isn't something you should generally change. +IMC2_PROTOCOL_VERSION = '2' + # Local time zone for this installation. All choices can be found here: # http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE TIME_ZONE = 'America/New_York' @@ -220,6 +249,7 @@ COMMAND_MODULES = ( 'src.commands.parents', 'src.commands.privileged', 'src.commands.search', + 'src.commands.imc2', ) """ diff --git a/src/imc2/__init__.py b/src/imc2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/imc2/connection.py b/src/imc2/connection.py new file mode 100644 index 000000000..fdaee233d --- /dev/null +++ b/src/imc2/connection.py @@ -0,0 +1,97 @@ +""" +IMC2 client module. Handles connecting to and communicating with an IMC2 server. +""" +import telnetlib +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 * + +# The active instance of IMC2Protocol. Set at server startup. +IMC2_PROTOCOL_INSTANCE = None + +class IMC2Protocol(StatefulTelnetProtocol): + """ + Provides the abstraction for the IMC2 protocol. Handles connection, + authentication, and all necessary packets. + """ + def __init__(self): + print "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 + self.auth_type = None + self.network_name = None + self.sequence = None + + def connectionMade(self): + """ + Triggered after connecting to the IMC2 network. + """ + print "IMC2: Connected to network server." + self.auth_type = "plaintext" + print "IMC2: Sending authentication packet." + self.send_packet(IMC2PacketAuthPlaintext()) + + def send_packet(self, packet): + """ + Given a sub-class of IMC2Packet, assemble the packet and send it + on its way. + """ + if self.sequence: + # This gets incremented with every command. + self.sequence += 1 + + packet.imc2_protocol = self + packet_str = packet.assemble() + print "IMC2: SENT> %s" % packet_str + self.sendLine(packet_str) + + def _parse_auth_response(self, line): + """ + Parses the IMC2 network authentication packet. + """ + if self.auth_type == "plaintext": + """ + SERVER Sends: PW version= + """ + if line[:2] == "PW": + line_split = line.split(' ') + self.network_name = line_split[4] + self.is_authenticated = True + self.sequence = time.time() + print "IMC2: Successfully authenticated to the '%s' network." % self.network_name + + def lineReceived(self, line): + """ + Triggered when text is received from the IMC2 network. Figures out + what to do with the packet. + """ + if not self.is_authenticated: + self._parse_auth_response(line) + else: + split_line = line.split(' ') + packet_type = split_line[3] + if packet_type == "is-alive": + pass + elif packet_type == "user-cache": + pass + else: + print "receive:", line + +class IMC2ClientFactory(ClientFactory): + """ + Creates instances of the IMC2Protocol. Should really only ever create one + in our particular instance. Tied in via src/server.py. + """ + protocol = IMC2Protocol + + def clientConnectionFailed(self, connector, reason): + print 'connection failed:', reason.getErrorMessage() + + def clientConnectionLost(self, connector, reason): + print 'connection lost:', reason.getErrorMessage() diff --git a/src/imc2/packets.py b/src/imc2/packets.py new file mode 100644 index 000000000..e92d59396 --- /dev/null +++ b/src/imc2/packets.py @@ -0,0 +1,109 @@ +""" +IMC2 packets. These are pretty well documented at: +http://www.mudbytes.net/index.php?a=articles&s=imc2_protocol +""" +from django.conf import settings + +class IMC2Packet(object): + """ + Base IMC2 packet class. This should never be used directly. Sub-class + and profit. + """ + # The following fields are all according to the basic packet format of: + # @ @ + sender = None + origin = settings.IMC2_MUDNAME + sequence = None + route = settings.IMC2_MUDNAME + packet_type = None + target = None + destination = None + # Optional data. + optional_data = {} + # Reference to the IMC2Protocol object doing the sending. + imc2_protocol = None + + def _get_optional_data_string(self): + """ + Generates the optional data string to tack on to the end of the packet. + """ + if self.optional_data: + data_string = '' + for key, value in self.optional_data.items(): + self.data_string += '%s=%s ' % (key, value) + return data_string.strip() + else: + return '' + + def _get_sender_name(self): + """ + Calculates the sender name to be sent with the packet. + """ + if self.sender: + name = self.sender.get_name(fullname=False, show_dbref=False, + show_flags=False, + no_ansi=True) + # IMC2 does not allow for spaces. + return name.strip().replace(' ', '_') + else: + return 'Unknown' + + def assemble(self): + """ + Assembles the packet and returns the ready-to-send string. + """ + self.sequence = self.imc2_protocol.sequence + packet = "%s@%s %s %s %s %s@%s %s\n" % ( + self._get_sender_name(), + self.origin, + self.sequence, + self.route, + self.packet_type, + self.target, + self.destination, + self._get_optional_data_string()) + return packet.strip() + +class IMC2PacketWhois(IMC2Packet): + """ + Description: + Sends a request to the network for the location of the specified player. + + Data: + level= The permission level of the person making the request. + + Example: + You@YourMUD 1234567890 YourMUD whois dude@* level=5 + """ + def __init__(self, pobject, whois_target): + self.sender = pobject + self.packet_type = 'whois' + self.target = whois_target + self.destination = '*' + self.data = {'level': '5'} + +class IMC2PacketAuthPlaintext(object): + """ + IMC2 plain-text authentication packet. Auth packets are strangely + formatted, so this does not sub-class IMC2Packet. The SHA and plain text + auth packets are the two only non-conformers. + + CLIENT Sends: + PW version= autosetup (SHA256) + + Optional Arguments( required if using the specified authentication method: + (SHA256) The literal string: SHA256. This is sent to notify the server + that the MUD is SHA256-Enabled. All future logins from this + client will be expected in SHA256-AUTH format if the server + supports it. + """ + def assemble(self): + """ + This is one of two strange packets, just assemble the packet manually + and go. + """ + return 'PW %s %s version=%s autosetup %s\n' %( + settings.IMC2_MUDNAME, + settings.IMC2_CLIENT_PW, + settings.IMC2_PROTOCOL_VERSION, + settings.IMC2_SERVER_PW) \ No newline at end of file diff --git a/src/server.py b/src/server.py index df818e969..d4fe1ca23 100755 --- a/src/server.py +++ b/src/server.py @@ -7,6 +7,7 @@ from django.db import connection from django.conf import settings from src.config.models import ConfigValue from src.session import SessionProtocol +from src.imc2.connection import IMC2ClientFactory from src import events from src import logger from src import session_mgr @@ -140,4 +141,14 @@ mud_service = EvenniaService() # Sheet sheet, fire ze missiles! serviceCollection = service.IServiceCollection(application) for port in settings.GAMEPORTS: - internet.TCPServer(port, mud_service.getEvenniaServiceFactory()).setServiceParent(serviceCollection) + internet.TCPServer(port, + mud_service.getEvenniaServiceFactory()).setServiceParent(serviceCollection) + + +if settings.IMC2_ENABLED: + imc2_factory = IMC2ClientFactory() + svc = internet.TCPClient(settings.IMC2_SERVER_ADDRESS, + settings.IMC2_SERVER_PORT, + imc2_factory) + svc.setName('IMC2') + svc.setServiceParent(serviceCollection) \ No newline at end of file