Reshuffling the Evennia package into the new template paradigm.
This commit is contained in:
parent
2846e64833
commit
2b3a32e447
371 changed files with 17250 additions and 304 deletions
1
lib/server/portal/__init__.py
Normal file
1
lib/server/portal/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
399
lib/server/portal/imc2.py
Normal file
399
lib/server/portal/imc2.py
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
"""
|
||||
IMC2 client module. Handles connecting to and communicating with an IMC2 server.
|
||||
"""
|
||||
|
||||
from time import time
|
||||
from twisted.internet import task
|
||||
from twisted.application import internet
|
||||
from twisted.internet import protocol
|
||||
from twisted.conch import telnet
|
||||
|
||||
from src.server.session import Session
|
||||
from src.utils import logger, utils
|
||||
from src.server.portal.imc2lib import imc2_ansi
|
||||
from src.server.portal.imc2lib import imc2_packets as pck
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
# storage containers for IMC2 muds and channels
|
||||
|
||||
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(dict):
|
||||
"""
|
||||
Keeps track of other MUDs connected to the IMC network.
|
||||
"""
|
||||
def get_mud_list(self):
|
||||
"""
|
||||
Returns a sorted list of connected Muds.
|
||||
"""
|
||||
muds = self.items()
|
||||
muds.sort()
|
||||
return [value for key, value in muds]
|
||||
|
||||
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.name] = mud
|
||||
|
||||
def remove_mud_from_packet(self, packet):
|
||||
"""
|
||||
Removes a mud from the Mud list when given a packet.
|
||||
"""
|
||||
mud = IMC2Mud(packet)
|
||||
try:
|
||||
del self[mud.name]
|
||||
except KeyError:
|
||||
# No matching entry, no big deal.
|
||||
pass
|
||||
|
||||
|
||||
class IMC2Channel(object):
|
||||
"""
|
||||
Stores information about channels available on the network.
|
||||
"""
|
||||
def __init__(self, packet):
|
||||
self.localname = packet.optional_data.get('localname', None)
|
||||
self.name = packet.optional_data.get('channel', None)
|
||||
self.level = packet.optional_data.get('level', None)
|
||||
self.owner = packet.optional_data.get('owner', None)
|
||||
self.policy = packet.optional_data.get('policy', None)
|
||||
self.last_updated = time()
|
||||
|
||||
|
||||
class IMC2ChanList(dict):
|
||||
"""
|
||||
Keeps track of Channels on the IMC network.
|
||||
"""
|
||||
|
||||
def get_channel_list(self):
|
||||
"""
|
||||
Returns a sorted list of cached channels.
|
||||
"""
|
||||
channels = self.items()
|
||||
channels.sort()
|
||||
return [value for key, value in channels]
|
||||
|
||||
def update_channel_from_packet(self, packet):
|
||||
"""
|
||||
This grabs relevant info from the packet and stuffs it in the
|
||||
channel list for later retrieval.
|
||||
"""
|
||||
channel = IMC2Channel(packet)
|
||||
self[channel.name] = channel
|
||||
|
||||
def remove_channel_from_packet(self, packet):
|
||||
"""
|
||||
Removes a channel from the Channel list when given a packet.
|
||||
"""
|
||||
channel = IMC2Channel(packet)
|
||||
try:
|
||||
del self[channel.name]
|
||||
except KeyError:
|
||||
# No matching entry, no big deal.
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# IMC2 protocol
|
||||
#
|
||||
|
||||
class IMC2Bot(telnet.StatefulTelnetProtocol, Session):
|
||||
"""
|
||||
Provides the abstraction for the IMC2 protocol. Handles connection,
|
||||
authentication, and all necessary packets.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.is_authenticated = False
|
||||
# only support plaintext passwords
|
||||
self.auth_type = "plaintext"
|
||||
self.sequence = None
|
||||
self.imc2_mudlist = IMC2MudList()
|
||||
self.imc2_chanlist = IMC2ChanList()
|
||||
|
||||
def _send_packet(self, packet):
|
||||
"Helper function to send packets across the wire"
|
||||
packet.imc2_protocol = self
|
||||
packet_str = utils.to_str(packet.assemble(self.factory.mudname,
|
||||
self.factory.client_pwd, self.factory.server_pwd))
|
||||
self.sendLine(packet_str)
|
||||
|
||||
def _isalive(self):
|
||||
"Send an isalive packet"
|
||||
self._send_packet(pck.IMC2PacketIsAlive())
|
||||
|
||||
def _keepalive(self):
|
||||
"Send a keepalive packet"
|
||||
# send to channel?
|
||||
self._send_packet(pck.IMC2PacketKeepAliveRequest())
|
||||
|
||||
def _channellist(self):
|
||||
"Sync the network channel list"
|
||||
checked_networks = []
|
||||
if not self.network in checked_networks:
|
||||
self._send_packet(pck.IMC2PacketIceRefresh())
|
||||
checked_networks.append(self.network)
|
||||
|
||||
def _prune(self):
|
||||
"Prune active channel list"
|
||||
t0 = time()
|
||||
for name, mudinfo in self.imc2_mudlist.items():
|
||||
if t0 - mudinfo.last_updated > 3599:
|
||||
del self.imc2_mudlist[name]
|
||||
|
||||
def _whois_reply(self, packet):
|
||||
"handle reply from server from an imcwhois request"
|
||||
# packet.target potentially contains the id of an character to target
|
||||
# not using that here
|
||||
response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown'))
|
||||
string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text}
|
||||
# somehow pass reply on to a given player, for now we just send to channel
|
||||
self.data_in(string)
|
||||
|
||||
def _format_tell(self, packet):
|
||||
"""
|
||||
Handle tells over IMC2 by formatting the text properly
|
||||
"""
|
||||
return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender": packet.sender,
|
||||
"origin": packet.origin,
|
||||
"msg": packet.optional_data.get('text', 'ERROR: No text provided.')}
|
||||
|
||||
def _imc_login(self, line):
|
||||
"Connect and identify to imc network"
|
||||
|
||||
if self.auth_type == "plaintext":
|
||||
# Only support Plain text passwords.
|
||||
# SERVER Sends: PW <servername> <serverpw> version=<version#> <networkname>
|
||||
|
||||
logger.log_infomsg("IMC2: AUTH< %s" % line)
|
||||
|
||||
line_split = line.split(' ')
|
||||
pw_present = line_split[0] == 'PW'
|
||||
autosetup_present = line_split[0] == 'autosetup'
|
||||
|
||||
if "reject" in line_split:
|
||||
auth_message = _("IMC2 server rejected connection.")
|
||||
logger.log_infomsg(auth_message)
|
||||
return
|
||||
|
||||
if pw_present:
|
||||
self.server_name = line_split[1]
|
||||
self.network_name = line_split[4]
|
||||
elif autosetup_present:
|
||||
logger.log_infomsg(_("IMC2: Autosetup response found."))
|
||||
self.server_name = line_split[1]
|
||||
self.network_name = line_split[3]
|
||||
self.is_authenticated = True
|
||||
self.sequence = int(time())
|
||||
|
||||
# Log to stdout and notify over MUDInfo.
|
||||
logger.log_infomsg('IMC2: Authenticated to %s' % self.factory.network)
|
||||
|
||||
# Ask to see what other MUDs are connected.
|
||||
self._send_packet(pck.IMC2PacketKeepAliveRequest())
|
||||
# IMC2 protocol states that KeepAliveRequests should be followed
|
||||
# up by the requester sending an IsAlive packet.
|
||||
self._send_packet(pck.IMC2PacketIsAlive())
|
||||
# Get a listing of channels.
|
||||
self._send_packet(pck.IMC2PacketIceRefresh())
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
Triggered after connecting to the IMC2 network.
|
||||
"""
|
||||
|
||||
self.stopping = False
|
||||
self.factory.bot = self
|
||||
address = "%s@%s" % (self.mudname, self.network)
|
||||
self.init_session("ircbot", address, self.factory.sessionhandler)
|
||||
# link back and log in
|
||||
self.uid = int(self.factory.uid)
|
||||
self.logged_in = True
|
||||
self.factory.sessionhandler.connect(self)
|
||||
logger.log_infomsg("IMC2 bot connected to %s." % self.network)
|
||||
# Send authentication packet. The reply will be caught by lineReceived
|
||||
self._send_packet(pck.IMC2PacketAuthPlaintext())
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""
|
||||
IMC2 -> Evennia
|
||||
|
||||
Triggered when text is received from the IMC2 network. Figures out
|
||||
what to do with the packet. This deals with the following
|
||||
|
||||
"""
|
||||
line = line.strip()
|
||||
|
||||
if not self.is_authenticated:
|
||||
# we are not authenticated yet. Deal with this.
|
||||
self._imc_login(line)
|
||||
return
|
||||
|
||||
#logger.log_infomsg("IMC2: RECV> %s" % line)
|
||||
|
||||
# Parse the packet and encapsulate it for easy access
|
||||
packet = pck.IMC2Packet(self.mudname, packet_str=line)
|
||||
|
||||
# Figure out what kind of packet we're dealing with and hand it
|
||||
# off to the correct handler.
|
||||
|
||||
if packet.packet_type == 'is-alive':
|
||||
self.imc2_mudlist.update_mud_from_packet(packet)
|
||||
elif packet.packet_type == 'keepalive-request':
|
||||
# Don't need to check the destination, we only receive these
|
||||
# packets when they are intended for us.
|
||||
self.send_packet(pck.IMC2PacketIsAlive())
|
||||
elif packet.packet_type == 'ice-msg-b':
|
||||
self.data_out(text=line, packettype="broadcast")
|
||||
elif packet.packet_type == 'whois-reply':
|
||||
# handle eventual whois reply
|
||||
self._whois_reply(packet)
|
||||
elif packet.packet_type == 'close-notify':
|
||||
self.imc2_mudlist.remove_mud_from_packet(packet)
|
||||
elif packet.packet_type == 'ice-update':
|
||||
self.imc2_chanlist.update_channel_from_packet(packet)
|
||||
elif packet.packet_type == 'ice-destroy':
|
||||
self.imc2_chanlist.remove_channel_from_packet(packet)
|
||||
elif packet.packet_type == 'tell':
|
||||
# send message to identified player
|
||||
pass
|
||||
|
||||
def data_in(self, text=None, **kwargs):
|
||||
"""
|
||||
Data IMC2 -> Evennia
|
||||
"""
|
||||
text = "bot_data_in " + text
|
||||
self.sessionhandler.data_in(self, text=text, **kwargs)
|
||||
|
||||
def data_out(self, text=None, **kwargs):
|
||||
"""
|
||||
Evennia -> IMC2
|
||||
|
||||
Keywords
|
||||
packet_type:
|
||||
broadcast - send to everyone on IMC channel
|
||||
tell - send a tell (see target keyword)
|
||||
whois - get whois information (see target keyword)
|
||||
sender - used by tell to identify the sender
|
||||
target - key identifier of target to tells or whois. If not
|
||||
given "Unknown" will be used.
|
||||
destination - used by tell to specify mud destination to send to
|
||||
|
||||
"""
|
||||
|
||||
if self.sequence:
|
||||
# This gets incremented with every command.
|
||||
self.sequence += 1
|
||||
|
||||
packet_type = kwargs.get("packet_type", "imcbroadcast")
|
||||
|
||||
if packet_type == "broadcast":
|
||||
# broadcast to everyone on IMC channel
|
||||
|
||||
if text.startswith("bot_data_out"):
|
||||
text = text.split(" ", 1)[1]
|
||||
else:
|
||||
return
|
||||
|
||||
# we remove the extra channel info since imc2 supplies this anyway
|
||||
if ":" in text:
|
||||
header, message = [part.strip() for part in text.split(":", 1)]
|
||||
# Create imc2packet and send it
|
||||
self._send_packet(pck.IMC2PacketIceMsgBroadcasted(self.servername,
|
||||
self.channel,
|
||||
header, text))
|
||||
elif packet_type == "tell":
|
||||
# send an IMC2 tell
|
||||
sender = kwargs.get("sender", self.mudname)
|
||||
target = kwargs.get("target", "Unknown")
|
||||
destination = kwargs.get("destination", "Unknown")
|
||||
self._send_packet(pck.IMC2PacketTell(sender, target, destination, text))
|
||||
|
||||
elif packet_type == "whois":
|
||||
# send a whois request
|
||||
sender = kwargs.get("sender", self.mudname)
|
||||
target = kwargs.get("target", "Unknown")
|
||||
self._send_packet(pck.IMC2PacketWhois(sender, target))
|
||||
|
||||
|
||||
class IMC2BotFactory(protocol.ReconnectingClientFactory):
|
||||
"""
|
||||
Creates instances of the IMC2Protocol. Should really only ever
|
||||
need to create one connection. Tied in via src/server.py.
|
||||
"""
|
||||
initialDelay = 1
|
||||
factor = 1.5
|
||||
maxDelay = 60
|
||||
|
||||
def __init__(self, sessionhandler, uid=None, network=None, channel=None,
|
||||
port=None, mudname=None, client_pwd=None, server_pwd=None):
|
||||
self.uid = uid
|
||||
self.network = network
|
||||
sname, host = network.split(".", 1)
|
||||
self.servername = sname.strip()
|
||||
self.channel = channel
|
||||
self.port = port
|
||||
self.mudname = mudname
|
||||
self.protocol_version = '2'
|
||||
self.client_pwd = client_pwd
|
||||
self.server_pwd = server_pwd
|
||||
self.bot = None
|
||||
self.task_isalive = None
|
||||
self.task_keepalive = None
|
||||
self.task_prune = None
|
||||
self.task_channellist = None
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"Build the protocol"
|
||||
protocol = IMC2Bot()
|
||||
protocol.factory = self
|
||||
protocol.network = self.network
|
||||
protocol.servername = self.servername
|
||||
protocol.channel = self.channel
|
||||
protocol.mudname = self.mudname
|
||||
protocol.port = self.port
|
||||
return protocol
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
self.retry(connector)
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
if not self.bot.stopping:
|
||||
self.retry(connector)
|
||||
|
||||
def start(self):
|
||||
"Connect session to sessionhandler"
|
||||
def errback(fail):
|
||||
logger.log_errmsg(fail.value)
|
||||
|
||||
if self.port:
|
||||
service = internet.TCPClient(self.network, int(self.port), self)
|
||||
self.sessionhandler.portal.services.addService(service)
|
||||
# start tasks
|
||||
self.task_isalive = task.LoopingCall(self.bot._isalive)
|
||||
self.task_keepalive = task.LoopingCall(self.bot._keepalive)
|
||||
self.task_prune = task.LoopingCall(self.bot._prune)
|
||||
self.task_channellist = task.LoopingCall(self.bot._channellist)
|
||||
self.task_isalive.start(900, now=False)
|
||||
self.task_keepalive.start(3500, now=False)
|
||||
self.task_prune.start(1800, now=False)
|
||||
self.task_channellist.start(3600 * 24, now=False)
|
||||
|
||||
1
lib/server/portal/imc2lib/__init__.py
Normal file
1
lib/server/portal/imc2lib/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
60
lib/server/portal/imc2lib/imc2_ansi.py
Normal file
60
lib/server/portal/imc2lib/imc2_ansi.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
ANSI parser - this adds colour to text according to
|
||||
special markup strings.
|
||||
|
||||
This is a IMC2 complacent version.
|
||||
"""
|
||||
|
||||
import re
|
||||
from src.utils import ansi
|
||||
|
||||
|
||||
class IMCANSIParser(ansi.ANSIParser):
|
||||
"""
|
||||
This parser is per the IMC2 specification.
|
||||
"""
|
||||
def __init__(self):
|
||||
normal = ansi.ANSI_NORMAL
|
||||
hilite = ansi.ANSI_HILITE
|
||||
self.ansi_map = [
|
||||
(r'~Z', normal), # Random
|
||||
(r'~x', normal + ansi.ANSI_BLACK), # Black
|
||||
(r'~D', hilite + ansi.ANSI_BLACK), # Dark Grey
|
||||
(r'~z', hilite + ansi.ANSI_BLACK),
|
||||
(r'~w', normal + ansi.ANSI_WHITE), # Grey
|
||||
(r'~W', hilite + ansi.ANSI_WHITE), # White
|
||||
(r'~g', normal + ansi.ANSI_GREEN), # Dark Green
|
||||
(r'~G', hilite + ansi.ANSI_GREEN), # Green
|
||||
(r'~p', normal + ansi.ANSI_MAGENTA), # Dark magenta
|
||||
(r'~m', normal + ansi.ANSI_MAGENTA),
|
||||
(r'~M', hilite + ansi.ANSI_MAGENTA), # Magenta
|
||||
(r'~P', hilite + ansi.ANSI_MAGENTA),
|
||||
(r'~c', normal + ansi.ANSI_CYAN), # Cyan
|
||||
(r'~y', normal + ansi.ANSI_YELLOW), # Dark Yellow (brown)
|
||||
(r'~Y', hilite + ansi.ANSI_YELLOW), # Yellow
|
||||
(r'~b', normal + ansi.ANSI_BLUE), # Dark Blue
|
||||
(r'~B', hilite + ansi.ANSI_BLUE), # Blue
|
||||
(r'~C', hilite + ansi.ANSI_BLUE),
|
||||
(r'~r', normal + ansi.ANSI_RED), # Dark Red
|
||||
(r'~R', hilite + ansi.ANSI_RED), # Red
|
||||
|
||||
## Formatting
|
||||
(r'~L', hilite), # Bold/hilite
|
||||
(r'~!', normal), # reset
|
||||
(r'\\r', normal),
|
||||
(r'\\n', ansi.ANSI_RETURN),
|
||||
]
|
||||
# prepare regex matching
|
||||
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
|
||||
for sub in self.ansi_map]
|
||||
# prepare matching ansi codes overall
|
||||
self.ansi_regex = re.compile("\033\[[0-9;]+m")
|
||||
|
||||
ANSI_PARSER = IMCANSIParser()
|
||||
|
||||
|
||||
def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER):
|
||||
"""
|
||||
Shortcut to use the IMC2 ANSI parser.
|
||||
"""
|
||||
return parser.parse_ansi(string, strip_ansi=strip_ansi)
|
||||
795
lib/server/portal/imc2lib/imc2_packets.py
Normal file
795
lib/server/portal/imc2lib/imc2_packets.py
Normal file
|
|
@ -0,0 +1,795 @@
|
|||
"""
|
||||
IMC2 packets. These are pretty well documented at:
|
||||
http://www.mudbytes.net/index.php?a=articles&s=imc2_protocol
|
||||
|
||||
"""
|
||||
import shlex
|
||||
from django.conf import settings
|
||||
|
||||
class Lexxer(shlex.shlex):
|
||||
"""
|
||||
A lexical parser for interpreting IMC2 packets.
|
||||
"""
|
||||
def __init__(self, packet_str, posix=True):
|
||||
shlex.shlex.__init__(self, packet_str, posix=True)
|
||||
# Single-quotes are notably not present. This is important!
|
||||
self.quotes = '"'
|
||||
self.commenters = ''
|
||||
# This helps denote what constitutes a continuous token.
|
||||
self.wordchars += "~`!@#$%^&*()-_+=[{]}|\\;:',<.>/?"
|
||||
|
||||
class IMC2Packet(object):
|
||||
"""
|
||||
Base IMC2 packet class. This is generally sub-classed, aside from using it
|
||||
to parse incoming packets from the IMC2 network server.
|
||||
"""
|
||||
def __init__(self, mudname=None, packet_str=None):
|
||||
"""
|
||||
Optionally, parse a packet and load it up.
|
||||
"""
|
||||
# The following fields are all according to the basic packet format of:
|
||||
# <sender>@<origin> <sequence> <route> <packet-type> <target>@<destination> <data...>
|
||||
self.sender = None
|
||||
if not mudname:
|
||||
mudname = settings.SERVERNAME
|
||||
self.origin = mudname
|
||||
self.sequence = None
|
||||
self.route = mudname
|
||||
self.packet_type = None
|
||||
self.target = None
|
||||
self.destination = None
|
||||
# Optional data.
|
||||
self.optional_data = {}
|
||||
# Reference to the IMC2Protocol object doing the sending.
|
||||
self.imc2_protocol = None
|
||||
|
||||
if packet_str:
|
||||
# The lexxer handles the double quotes correctly, unlike just
|
||||
# splitting. Spaces throw things off, so shlex handles it
|
||||
# gracefully, ala POSIX shell-style parsing.
|
||||
lex = Lexxer(packet_str)
|
||||
|
||||
# Token counter.
|
||||
counter = 0
|
||||
for token in lex:
|
||||
if counter == 0:
|
||||
# This is the sender@origin token.
|
||||
sender_origin = token
|
||||
split_sender_origin = sender_origin.split('@')
|
||||
self.sender = split_sender_origin[0].strip()
|
||||
self.origin = split_sender_origin[1]
|
||||
elif counter == 1:
|
||||
# Numeric time-based sequence.
|
||||
self.sequence = token
|
||||
elif counter == 2:
|
||||
# Packet routing info.
|
||||
self.route = token
|
||||
elif counter == 3:
|
||||
# Packet type string.
|
||||
self.packet_type = token
|
||||
elif counter == 4:
|
||||
# Get values for the target and destination attributes.
|
||||
target_destination = token
|
||||
split_target_destination = target_destination.split('@')
|
||||
self.target = split_target_destination[0]
|
||||
try:
|
||||
self.destination = split_target_destination[1]
|
||||
except IndexError:
|
||||
# There is only one element to the target@dest segment
|
||||
# of the packet. Wipe the target and move the captured
|
||||
# value to the destination attrib.
|
||||
self.target = '*'
|
||||
self.destination = split_target_destination[0]
|
||||
elif counter > 4:
|
||||
# Populate optional data.
|
||||
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
|
||||
|
||||
def __str__(self):
|
||||
retval = """
|
||||
--IMC2 package (%s)
|
||||
Sender: %s
|
||||
Origin: %s
|
||||
Sequence: %s
|
||||
Route: %s
|
||||
Type: %s
|
||||
Target: %s
|
||||
Dest.: %s
|
||||
Data:
|
||||
%s
|
||||
------------------------""" % (self.packet_type, self.sender,
|
||||
self.origin, self.sequence,
|
||||
self.route, self.packet_type,
|
||||
self.target, self.destination,
|
||||
"\n ".join(["%s: %s" % items for items in self.optional_data.items()]))
|
||||
return retval.strip()
|
||||
|
||||
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():
|
||||
# Determine the number of words in this value.
|
||||
words = len(str(value).split(' '))
|
||||
# Anything over 1 word needs double quotes.
|
||||
if words > 1:
|
||||
value = '"%s"' % (value,)
|
||||
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 == '*':
|
||||
# Some packets have no sender.
|
||||
return '*'
|
||||
elif str(self.sender).isdigit():
|
||||
return self.sender
|
||||
elif type(self.sender) in [type(u""),type(str())]:
|
||||
#this is used by e.g. IRC where no user object is present.
|
||||
return self.sender.strip().replace(' ', '_')
|
||||
elif self.sender:
|
||||
# Player object.
|
||||
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:
|
||||
# None value. Do something or other.
|
||||
return 'Unknown'
|
||||
|
||||
def assemble(self, mudname=None, client_pwd=None, server_pwd=None):
|
||||
"""
|
||||
Assembles the packet and returns the ready-to-send string.
|
||||
Note that the arguments are not used, they are there for
|
||||
consistency across all packets.
|
||||
"""
|
||||
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 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 <mudname> <clientpw> version=<version#> autosetup <serverpw> (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, mudname=None, client_pwd=None, server_pwd=None):
|
||||
"""
|
||||
This is one of two strange packets, just assemble the packet manually
|
||||
and go.
|
||||
"""
|
||||
return 'PW %s %s version=2 autosetup %s\n' %(mudname, client_pwd, server_pwd)
|
||||
|
||||
|
||||
class IMC2PacketKeepAliveRequest(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is sent by a MUD to trigger is-alive packets from other MUDs.
|
||||
This packet is usually followed by the sending MUD's own is-alive packet.
|
||||
It is used in the filling of a client's MUD list, thus any MUD that doesn't
|
||||
respond with an is-alive isn't marked as online on the sending MUD's
|
||||
mudlist.
|
||||
|
||||
Data:
|
||||
(none)
|
||||
|
||||
Example of a received keepalive-request:
|
||||
*@YourMUD 1234567890 YourMUD!Hub1 keepalive-request *@*
|
||||
|
||||
Example of a sent keepalive-request:
|
||||
*@YourMUD 1234567890 YourMUD keepalive-request *@*
|
||||
"""
|
||||
def __init__(self):
|
||||
super(IMC2PacketKeepAliveRequest, self).__init__()
|
||||
self.sender = '*'
|
||||
self.packet_type = 'keepalive-request'
|
||||
self.target = '*'
|
||||
self.destination = '*'
|
||||
|
||||
|
||||
class IMC2PacketIsAlive(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is the reply to a keepalive-request packet. It is responsible
|
||||
for filling a client's mudlist with the information about other MUDs on the
|
||||
network.
|
||||
|
||||
Data:
|
||||
versionid=<string>
|
||||
Where <string> is the text version ID of the client. ("IMC2 4.5 MUD-Net")
|
||||
|
||||
url=<string>
|
||||
Where <string> is the proper URL of the client. (http://www.domain.com)
|
||||
|
||||
host=<string>
|
||||
Where <string> is the telnet address of the MUD. (telnet://domain.com)
|
||||
|
||||
port=<int>
|
||||
Where <int> is the telnet port of the MUD.
|
||||
|
||||
(These data fields are not sent by the MUD, they are added by the server.)
|
||||
networkname=<string>
|
||||
Where <string> is the network name that the MUD/server is on. ("MyNetwork")
|
||||
|
||||
sha256=<int>
|
||||
This is an optional tag that denotes the SHA-256 capabilities of a
|
||||
MUD or server.
|
||||
|
||||
Example of a received is-alive:
|
||||
*@SomeMUD 1234567890 SomeMUD!Hub2 is-alive *@YourMUD versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" networkname="MyNetwork" sha256=1 host=domain.com port=5500
|
||||
|
||||
Example of a sent is-alive:
|
||||
*@YourMUD 1234567890 YourMUD is-alive *@* versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" host=domain.com port=5500
|
||||
"""
|
||||
def __init__(self):
|
||||
super(IMC2PacketIsAlive, self).__init__()
|
||||
self.sender = '*'
|
||||
self.packet_type = 'is-alive'
|
||||
self.target = '*'
|
||||
self.destination = '*'
|
||||
self.optional_data = {'versionid': 'Evennia IMC2',
|
||||
'url': '"http://www.evennia.com"',
|
||||
'host': 'test.com',
|
||||
'port': '5555'}
|
||||
|
||||
|
||||
class IMC2PacketIceRefresh(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is sent by the MUD to request data about the channels on the
|
||||
network. Servers with channels reply with an ice-update packet for each
|
||||
channel they control. The usual target for this packet is IMC@$.
|
||||
|
||||
Data:
|
||||
(none)
|
||||
|
||||
Example:
|
||||
*@YourMUD 1234567890 YourMUD!Hub1 ice-refresh IMC@$
|
||||
"""
|
||||
def __init__(self):
|
||||
super(IMC2PacketIceRefresh, self).__init__()
|
||||
self.sender = '*'
|
||||
self.packet_type = 'ice-refresh'
|
||||
self.target = 'IMC'
|
||||
self.destination = '$'
|
||||
|
||||
|
||||
class IMC2PacketIceUpdate(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
A server returns this packet with the data of a channel when prompted with
|
||||
an ice-refresh request.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The channel's network name in the format of ServerName:ChannelName
|
||||
|
||||
owner=<string>
|
||||
The Name@MUD of the channel's owner
|
||||
|
||||
operators=<string>
|
||||
A space-seperated list of the Channel's operators, (format: Person@MUD)
|
||||
|
||||
policy=<string>
|
||||
The policy is either "open" or "private" with no quotes.
|
||||
|
||||
invited=<string>
|
||||
The space-seperated list of invited User@MUDs, only valid for a
|
||||
"private" channel.
|
||||
|
||||
excluded=<string>
|
||||
The space-seperated list of banned User@MUDs, only valid for "open"
|
||||
channels.
|
||||
|
||||
level=<string> The default level of the channel: Admin, Imp, Imm,
|
||||
Mort, or None
|
||||
|
||||
localname=<string> The suggested local name of the channel.
|
||||
|
||||
Examples:
|
||||
|
||||
Open Policy:
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:ichat owner=Imm@SomeMUD operators=Other@SomeMUD policy=open excluded="Flamer@badMUD Jerk@dirtyMUD" level=Imm localname=ichat
|
||||
|
||||
Private Policy:
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:secretchat owner=Imm@SomeMUD operators=Other@SomeMUD policy=private invited="SpecialDude@OtherMUD CoolDude@WeirdMUD" level=Mort localname=schat
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketIceMsgRelayed(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The -r in this ice-msg packet means it was relayed. This, along with the
|
||||
ice-msg-p packet, are used with private policy channels. The 'r' stands
|
||||
for 'relay'. All incoming channel messages are from ICE@<server>, where
|
||||
<server> is the server hosting the channel.
|
||||
|
||||
Data:
|
||||
realfrom=<string>
|
||||
The User@MUD the message came from.
|
||||
|
||||
channel=<string>
|
||||
The Server:Channel the message is intended to be displayed on.
|
||||
|
||||
text=<string>
|
||||
The message text.
|
||||
|
||||
emote=<int>
|
||||
An integer value designating emotes. 0 for no emote, 1 for an emote,
|
||||
and 2 for a social.
|
||||
|
||||
Examples:
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="Aha! I got it!" emote=0
|
||||
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text=Ahh emote=0
|
||||
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="grins evilly." emote=1
|
||||
|
||||
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="You@YourMUD grins evilly!" emote=2
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketIceMsgPrivate(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is sent when a player sends a message to a private channel.
|
||||
This packet should never be seen as incoming to a client. The target of
|
||||
this packet should be IMC@<server> of the server hosting the channel.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The Server:Channel the message is intended to be displayed on.
|
||||
|
||||
text=<string>
|
||||
The message text.
|
||||
|
||||
emote=<int>
|
||||
An integer value designating emotes. 0 for no emote, 1 for an emote,
|
||||
and 2 for a social.
|
||||
|
||||
echo=<int>
|
||||
Tells the server to echo the message back to the sending MUD. This is only
|
||||
seen on out-going messages.
|
||||
|
||||
Examples:
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="Ahh! I got it!" emote=0 echo=1
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text=Ahh! emote=0 echo=1
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="grins evilly." emote=1 echo=1
|
||||
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="You@YourMUD grins evilly." emote=2 echo=1
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketIceMsgBroadcasted(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This is the packet used to chat on open policy channels. When sent from a
|
||||
MUD, it is broadcasted across the network. Other MUDs receive it in-tact
|
||||
as it was sent by the originating MUD. The server that hosts the channel
|
||||
sends the packet back to the originating MUD as an 'echo' by removing the
|
||||
"echo=1" and attaching the "sender=Person@MUD" data field.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The Server:Channel the message is intended to be displayed on.
|
||||
|
||||
text=<string>
|
||||
The message text.
|
||||
|
||||
emote=<int>
|
||||
An integer value designating emotes. 0 for no emote, 1 for an emote,
|
||||
and 2 for a social.
|
||||
|
||||
*echo=<int>
|
||||
This stays on broadcasted messages. It tells the channel's server to
|
||||
relay an echo back.
|
||||
|
||||
*sender=<string>
|
||||
The hosting server replaces "echo=1" with this when sending the echo back
|
||||
to the originating MUD.
|
||||
|
||||
Examples:
|
||||
(See above for emote/social examples as they are pretty much the same)
|
||||
|
||||
Return Echo Packet:
|
||||
You-YourMUD@Hub1 1234567890 Hub1 ice-msg-b *@YourMUD text=Hi! channel=Hub1:ichat sender=You@YourMUD emote=0
|
||||
|
||||
Broadcasted Packet:
|
||||
You@YourMUD 1234567890 YourMUD!Hub1 ice-msg-b *@* channel=Hub1:ichat text=Hi! emote=0 echo=1
|
||||
"""
|
||||
def __init__(self, server, channel, pobject, message):
|
||||
"""
|
||||
Args:
|
||||
server: (String) Server name the channel resides on (obs - this is
|
||||
e.g. Server01, not the full network name!)
|
||||
channel: (String) Name of the IMC2 channel.
|
||||
pobject: (Object) Object sending the message.
|
||||
message: (String) Message to send.
|
||||
"""
|
||||
super(IMC2PacketIceMsgBroadcasted, self).__init__()
|
||||
self.sender = pobject
|
||||
self.packet_type = 'ice-msg-b'
|
||||
self.target = '*'
|
||||
self.destination = '*'
|
||||
self.optional_data = {'channel': '%s:%s' % (server, channel),
|
||||
'text': message,
|
||||
'emote': 0,
|
||||
'echo': 1}
|
||||
|
||||
|
||||
class IMC2PacketUserCache(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sent by a MUD with a new IMC2-able player or when a player's gender changes,
|
||||
this packet contains only the gender for data. The packet's origination
|
||||
should be the Player@MUD.
|
||||
|
||||
Data:
|
||||
gender=<int> 0 is male, 1 is female, 2 is anything else such as neuter.
|
||||
Will be referred to as "it".
|
||||
|
||||
Example:
|
||||
Dude@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache *@* gender=0
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketUserCacheRequest(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The MUD sends this packet out when making a request for the user-cache
|
||||
information of the user included in the data part of the packet.
|
||||
|
||||
Data:
|
||||
user=<string> The Person@MUD whose data the MUD is seeking.
|
||||
|
||||
Example:
|
||||
*@YourMUD 1234567890 YourMUD user-cache-request *@SomeMUD user=Dude@SomeMUD
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketUserCacheReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
A reply to the user-cache-request packet. It contains the user and gender
|
||||
for the user.
|
||||
|
||||
Data:
|
||||
user=<string>
|
||||
The Person@MUD whose data the MUD requested.
|
||||
|
||||
gender=<int>
|
||||
The gender of the Person@MUD in the 'user' field.
|
||||
|
||||
Example:
|
||||
*@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache-reply *@YourMUD user=Dude@SomeMUD gender=0
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketTell(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is used to communicate private messages between users on MUDs
|
||||
across the network.
|
||||
|
||||
Data:
|
||||
text=<string> Message text
|
||||
isreply=<int> Two settings: 1 denotes a reply, 2 denotes a tell social.
|
||||
|
||||
Example:
|
||||
|
||||
Originating:
|
||||
You@YourMUD 1234567890 YourMUD tell Dude@SomeMUD text="Having fun?"
|
||||
|
||||
Reply from Dude:
|
||||
Dude@SomeMUD 1234567890 SomeMUD!Hub1 tell You@YourMUD text="Yeah, this is cool!" isreply=1
|
||||
"""
|
||||
def __init__(self, pobject, target, destination, message):
|
||||
super(IMC2PacketTell, self).__init__()
|
||||
self.sender = pobject
|
||||
self.packet_type = "tell"
|
||||
self.target = target
|
||||
self.destination = destination
|
||||
self.optional_data = {"text": message,
|
||||
"isreply":None}
|
||||
|
||||
def assemble(self, mudname=None, client_pwd=None, server_pwd=None):
|
||||
self.sequence = self.imc2_protocol.sequence
|
||||
#self.route = "%s!%s" % (self.origin, self.imc2_protocol.factory.servername.capitalize())
|
||||
return '''"%s@%s %s %s tell %s@%s text="%s"''' % (self.sender, self.origin, self.sequence,
|
||||
self.route, self.target, self.destination,
|
||||
self.optional_data.get("text","NO TEXT GIVEN"))
|
||||
|
||||
|
||||
class IMC2PacketEmote(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet seems to be sent by servers when notifying the network of a new
|
||||
channel or the destruction of a channel.
|
||||
|
||||
Data:
|
||||
channel=<int>
|
||||
Unsure of what this means. The channel seen in both creation and
|
||||
destruction packets is 15.
|
||||
|
||||
level=<int>
|
||||
I am assuming this is the permission level of the sender. In both
|
||||
creation and destruction messages, this is -1.
|
||||
|
||||
text=<string>
|
||||
This is the message to be sent to the users.
|
||||
|
||||
Examples:
|
||||
ICE@Hub1 1234567890 Hub1 emote *@* channel=15 level=-1 text="the
|
||||
channel called hub1:test has been destroyed by You@YourMUD."
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketRemoteAdmin(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet is used in remote server administration. Please note that
|
||||
SHA-256 Support is *required* for a client to use this feature. The command
|
||||
can vary, in fact this very packet is highly dependant on the server it's
|
||||
being directed to. In most cases, sending the 'list' command will have a
|
||||
remote-admin enabled server send you the list of commands it will accept.
|
||||
|
||||
Data:
|
||||
command=<string>
|
||||
The command being sent to the server for processing.
|
||||
|
||||
data=<string>
|
||||
Data associated with the command. This is not always required.
|
||||
|
||||
hash=<string>
|
||||
The SHA-256 hash that is verified by the server. This hash is generated in
|
||||
the same manner as an authentication packet.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD remote-admin IMC@Hub1 command=list hash=<hash goes here>
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketIceCmd(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Used for remote channel administration. In most cases, one must be listed
|
||||
as a channel creator on the target server in order to do much with this
|
||||
packet. Other cases include channel operators.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The target server:channel for the command.
|
||||
|
||||
command=<string>
|
||||
The command to be processed.
|
||||
|
||||
data=<string>
|
||||
Data associated with the command. This is not always required.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD ice-cmd IMC@hub1 channel=hub1:ichat command=list
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketDestroy(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sent by a server to indicate the destruction of a channel it hosted.
|
||||
The mud should remove this channel from its local configuration.
|
||||
|
||||
Data:
|
||||
channel=<string> The server:channel being destroyed.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketWho(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
A seemingly mutli-purpose information-requesting packet. The istats
|
||||
packet currently only works on servers, or at least that's the case on
|
||||
MUD-Net servers. The 'finger' type takes a player name in addition to the
|
||||
type name.
|
||||
|
||||
Example: "finger Dude". The 'who' and 'info' types take no argument.
|
||||
The MUD is responsible for building the reply text sent in the who-reply
|
||||
packet.
|
||||
|
||||
Data:
|
||||
type=<string> Types: who, info, "finger <name>", istats (server only)
|
||||
|
||||
Example:
|
||||
Dude@SomeMUD 1234567890 SomeMUD!Hub1 who *@YourMUD type=who
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketWhoReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The multi-purpose reply to the multi-purpose information-requesting 'who'
|
||||
packet. The MUD is responsible for building the return data, including the
|
||||
format of it. The mud can use the permission level sent in the original who
|
||||
packet to filter the output. The example below is the MUD-Net format.
|
||||
|
||||
Data:
|
||||
text=<string> The formatted reply to a 'who' packet.
|
||||
|
||||
Additional Notes:
|
||||
The example below is for the who list packet. The same construction would
|
||||
go into formatting the other types of who packets.
|
||||
|
||||
Example:
|
||||
*@YourMUD 1234567890 YourMUD who-reply Dude@SomeMUD text="\n\r~R-=< ~WPlayers on YourMUD ~R>=-\n\r ~Y-=< ~Wtelnet://yourmud.domain.com:1234 ~Y>=-\n\r\n\r~B--------------------------------=< ~WPlayers ~B>=---------------------------------\n\r\n\r ~BPlayer ~z<--->~G Mortal the Toy\n\r\n\r~R-------------------------------=< ~WImmortals ~R>=--------------------------------\n\r\n\r ~YStaff ~z<--->~G You the Immortal\n\r\n\r~Y<~W2 Players~Y> ~Y<~WHomepage: http://www.yourmud.com~Y> <~W 2 Max Since Reboot~Y>\n\r~Y<~W3 logins since last reboot on Tue Feb 24, 2004 6:55:59 PM EST~Y>"
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketWhois(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sends a request to the network for the location of the specified player.
|
||||
|
||||
Data:
|
||||
level=<int> The permission level of the person making the request.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD whois dude@* level=5
|
||||
"""
|
||||
def __init__(self, pobject_id, whois_target):
|
||||
super(IMC2PacketWhois, self).__init__()
|
||||
# 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 = '*'
|
||||
self.optional_data = {'level': '5'}
|
||||
|
||||
|
||||
class IMC2PacketWhoisReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
The reply to a whois packet. The MUD is responsible for building and formatting
|
||||
the text sent back to the requesting player, and can use the permission level
|
||||
sent in the original whois packet to filter or block the response.
|
||||
|
||||
Data:
|
||||
text=<string> The whois text.
|
||||
|
||||
Example:
|
||||
*@SomeMUD 1234567890 SomeMUD!Hub1 whois-reply You@YourMUD text="~RIMC Locate: ~YDude@SomeMUD: ~cOnline.\n\r"
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketBeep(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sends out a beep packet to the Player@MUD. The client receiving this should
|
||||
then send a bell-character to the target player to 'beep' them.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD beep dude@somemud
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketIceChanWho(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
Sends a request to the specified MUD or * to list all the users listening
|
||||
to the specified channel.
|
||||
|
||||
Data:
|
||||
level=<int>
|
||||
Sender's permission level.
|
||||
|
||||
channel=<string>
|
||||
The server:chan name of the channel.
|
||||
|
||||
lname=<string>
|
||||
The localname of the channel.
|
||||
|
||||
Example:
|
||||
You@YourMUD 1234567890 YourMUD ice-chan-who somemud level=5 channel=Hub1:ichat lname=ichat
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketIceChanWhoReply(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This is the reply packet for an ice-chan-who. The MUD is responsible for
|
||||
creating and formatting the list sent back in the 'list' field. The
|
||||
permission level sent in the original ice-chan-who packet can be used to
|
||||
filter or block the response.
|
||||
|
||||
Data:
|
||||
channel=<string>
|
||||
The server:chan of the requested channel.
|
||||
|
||||
list=<string>
|
||||
The formatted list of local listeners for that MUD.
|
||||
|
||||
Example:
|
||||
*@SomeMUD 1234567890 SomeMUD!Hub1 ice-chan-whoreply You@YourMUD channel=Hub1:ichat list="The following people are listening to ichat on SomeMUD:\n\r\n\rDude\n\r"
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketLaston(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet queries the server the mud is connected to to find out when a
|
||||
specified user was last seen by the network on a public channel.
|
||||
|
||||
Data:
|
||||
username=<string> The user, user@mud, or "all" being queried. Responses
|
||||
to this packet will be sent by the server in the form of a series of tells.
|
||||
|
||||
Example: User@MUD 1234567890 MUD imc-laston SERVER username=somenamehere
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IMC2PacketCloseNotify(IMC2Packet):
|
||||
"""
|
||||
Description:
|
||||
This packet alerts the network when a server or MUD has disconnected. The
|
||||
server hosting the server or MUD is responsible for sending this packet
|
||||
out across the network. Clients need only process the packet to remove the
|
||||
disconnected MUD from their MUD list (or mark it as Disconnected).
|
||||
|
||||
Data:
|
||||
host=<string>
|
||||
The MUD or server that has disconnected from the network.
|
||||
|
||||
Example:
|
||||
*@Hub2 1234567890 Hub2!Hub1 close-notify *@* host=DisconnMUD
|
||||
"""
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
packstr = "Kayle@MW 1234567 MW!Server02!Server01 ice-msg-b *@* channel=Server01:ichat text=\"*they're going woot\" emote=0 echo=1"
|
||||
packstr = "*@Lythelian 1234567 Lythelian!Server01 is-alive *@* versionid=\"Tim's LPC IMC2 client 30-Jan-05 / Dead Souls integrated\" networkname=Mudbytes url=http://dead-souls.net host=70.32.76.142 port=6666 sha256=0"
|
||||
print IMC2Packet(packstr)
|
||||
|
||||
125
lib/server/portal/irc.py
Normal file
125
lib/server/portal/irc.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
This connects to an IRC network/channel and launches an 'bot' onto it.
|
||||
The bot then pipes what is being said between the IRC channel and one or
|
||||
more Evennia channels.
|
||||
"""
|
||||
|
||||
from twisted.application import internet
|
||||
from twisted.words.protocols import irc
|
||||
from twisted.internet import protocol
|
||||
from src.server.session import Session
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
# IRC bot
|
||||
|
||||
class IRCBot(irc.IRCClient, Session):
|
||||
"""
|
||||
An IRC bot that tracks actitivity in a channel as well
|
||||
as sends text to it when prompted
|
||||
"""
|
||||
lineRate = 1
|
||||
|
||||
# assigned by factory at creation
|
||||
|
||||
nickname = None
|
||||
logger = None
|
||||
factory = None
|
||||
channel = None
|
||||
|
||||
def signedOn(self):
|
||||
"""
|
||||
This is called when we successfully connect to
|
||||
the network. We make sure to now register with
|
||||
the game as a full session.
|
||||
"""
|
||||
self.join(self.channel)
|
||||
self.stopping = False
|
||||
self.factory.bot = self
|
||||
address = "%s@%s" % (self.channel, self.network)
|
||||
self.init_session("ircbot", address, self.factory.sessionhandler)
|
||||
# we link back to our bot and log in
|
||||
self.uid = int(self.factory.uid)
|
||||
self.logged_in = True
|
||||
self.factory.sessionhandler.connect(self)
|
||||
logger.log_infomsg("IRC bot '%s' connected to %s at %s:%s." % (self.nickname, self.channel,
|
||||
self.network, self.port))
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Called by sessionhandler to disconnect this protocol
|
||||
"""
|
||||
print "irc disconnect called!"
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.stopping = True
|
||||
self.transport.loseConnection()
|
||||
|
||||
def privmsg(self, user, channel, msg):
|
||||
"A message was sent to channel"
|
||||
if not msg.startswith('***'):
|
||||
user = user.split('!', 1)[0]
|
||||
self.data_in("bot_data_in %s@%s: %s" % (user, channel, msg))
|
||||
|
||||
def action(self, user, channel, msg):
|
||||
"An action was done in channel"
|
||||
if not msg.startswith('**'):
|
||||
user = user.split('!', 1)[0]
|
||||
self.data_in("bot_data_in %s@%s %s" % (user, channel, msg))
|
||||
|
||||
def data_in(self, text=None, **kwargs):
|
||||
"Data IRC -> Server"
|
||||
self.sessionhandler.data_in(self, text=text, **kwargs)
|
||||
|
||||
def data_out(self, text=None, **kwargs):
|
||||
"Data from server-> IRC"
|
||||
if text.startswith("bot_data_out"):
|
||||
text = text.split(" ", 1)[1]
|
||||
self.say(self.channel, text)
|
||||
|
||||
|
||||
class IRCBotFactory(protocol.ReconnectingClientFactory):
|
||||
"""
|
||||
Creates instances of AnnounceBot, connecting with
|
||||
a staggered increase in delay
|
||||
"""
|
||||
# scaling reconnect time
|
||||
initialDelay = 1
|
||||
factor = 1.5
|
||||
maxDelay = 60
|
||||
|
||||
def __init__(self, sessionhandler, uid=None, botname=None, channel=None, network=None, port=None):
|
||||
"Storing some important protocol properties"
|
||||
self.sessionhandler = sessionhandler
|
||||
self.uid = uid
|
||||
self.nickname = str(botname)
|
||||
self.channel = str(channel)
|
||||
self.network = str(network)
|
||||
self.port = port
|
||||
self.bot = None
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"Build the protocol and assign it some properties"
|
||||
protocol = IRCBot()
|
||||
protocol.factory = self
|
||||
protocol.nickname = self.nickname
|
||||
protocol.channel = self.channel
|
||||
protocol.network = self.network
|
||||
protocol.port = self.port
|
||||
return protocol
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
"Tracks reconnections for debugging"
|
||||
logger.log_infomsg("(re)connecting to %s" % self.channel)
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
self.retry(connector)
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
if not self.bot.stopping:
|
||||
self.retry(connector)
|
||||
|
||||
def start(self):
|
||||
"Connect session to sessionhandler"
|
||||
if self.port:
|
||||
service = internet.TCPClient(self.network, int(self.port), self)
|
||||
self.sessionhandler.portal.services.addService(service)
|
||||
67
lib/server/portal/mccp.py
Normal file
67
lib/server/portal/mccp.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
|
||||
MCCP - Mud Client Compression Protocol
|
||||
|
||||
This implements the MCCP v2 telnet protocol as per
|
||||
http://tintin.sourceforge.net/mccp/. MCCP allows for the server to
|
||||
compress data when sending to supporting clients, reducing bandwidth
|
||||
by 70-90%.. The compression is done using Python's builtin zlib
|
||||
library. If the client doesn't support MCCP, server sends uncompressed
|
||||
as normal. Note: On modern hardware you are not likely to notice the
|
||||
effect of MCCP unless you have extremely heavy traffic or sits on a
|
||||
terribly slow connection.
|
||||
|
||||
This protocol is implemented by the telnet protocol importing
|
||||
mccp_compress and calling it from its write methods.
|
||||
"""
|
||||
import zlib
|
||||
|
||||
# negotiations for v1 and v2 of the protocol
|
||||
MCCP = chr(86)
|
||||
FLUSH = zlib.Z_SYNC_FLUSH
|
||||
|
||||
|
||||
def mccp_compress(protocol, data):
|
||||
"Handles zlib compression, if applicable"
|
||||
if hasattr(protocol, 'zlib'):
|
||||
return protocol.zlib.compress(data) + protocol.zlib.flush(FLUSH)
|
||||
return data
|
||||
|
||||
|
||||
class Mccp(object):
|
||||
"""
|
||||
Implements the MCCP protocol. Add this to a
|
||||
variable on the telnet protocol to set it up.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
initialize MCCP by storing protocol on
|
||||
ourselves and calling the client to see if
|
||||
it supports MCCP. Sets callbacks to
|
||||
start zlib compression in that case.
|
||||
"""
|
||||
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags['MCCP'] = False
|
||||
# ask if client will mccp, connect callbacks to handle answer
|
||||
self.protocol.will(MCCP).addCallbacks(self.do_mccp, self.no_mccp)
|
||||
|
||||
def no_mccp(self, option):
|
||||
"""
|
||||
Called if client doesn't support mccp or chooses to turn it off
|
||||
"""
|
||||
if hasattr(self.protocol, 'zlib'):
|
||||
del self.protocol.zlib
|
||||
self.protocol.protocol_flags['MCCP'] = False
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_mccp(self, option):
|
||||
"""
|
||||
The client supports MCCP. Set things up by
|
||||
creating a zlib compression stream.
|
||||
"""
|
||||
self.protocol.protocol_flags['MCCP'] = True
|
||||
self.protocol.requestNegotiation(MCCP, '')
|
||||
self.protocol.zlib = zlib.compressobj(9)
|
||||
self.protocol.handshake_done()
|
||||
243
lib/server/portal/msdp.py
Normal file
243
lib/server/portal/msdp.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
"""
|
||||
|
||||
MSDP (Mud Server Data Protocol)
|
||||
|
||||
This implements the MSDP protocol as per
|
||||
http://tintin.sourceforge.net/msdp/. MSDP manages out-of-band
|
||||
communication between the client and server, for updating health bars
|
||||
etc.
|
||||
|
||||
"""
|
||||
import re
|
||||
from src.utils.utils import to_str
|
||||
|
||||
# MSDP-relevant telnet cmd/opt-codes
|
||||
MSDP = chr(69)
|
||||
MSDP_VAR = chr(1)
|
||||
MSDP_VAL = chr(2)
|
||||
MSDP_TABLE_OPEN = chr(3)
|
||||
MSDP_TABLE_CLOSE = chr(4)
|
||||
MSDP_ARRAY_OPEN = chr(5)
|
||||
MSDP_ARRAY_CLOSE = chr(6)
|
||||
|
||||
IAC = chr(255)
|
||||
SB = chr(250)
|
||||
SE = chr(240)
|
||||
|
||||
force_str = lambda inp: to_str(inp, force_string=True)
|
||||
|
||||
# pre-compiled regexes
|
||||
# returns 2-tuple
|
||||
regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
|
||||
MSDP_ARRAY_OPEN,
|
||||
MSDP_ARRAY_CLOSE))
|
||||
# returns 2-tuple (may be nested)
|
||||
regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
|
||||
MSDP_TABLE_OPEN,
|
||||
MSDP_TABLE_CLOSE))
|
||||
regex_var = re.compile(MSDP_VAR)
|
||||
regex_val = re.compile(MSDP_VAL)
|
||||
|
||||
|
||||
# Msdp object handler
|
||||
|
||||
class Msdp(object):
|
||||
"""
|
||||
Implements the MSDP protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
Initiates by storing the protocol
|
||||
on itself and trying to determine
|
||||
if the client supports MSDP.
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags['MSDP'] = False
|
||||
self.protocol.negotiationMap[MSDP] = self.msdp_to_evennia
|
||||
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
|
||||
self.msdp_reported = {}
|
||||
|
||||
def no_msdp(self, option):
|
||||
"No msdp supported or wanted"
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_msdp(self, option):
|
||||
"""
|
||||
Called when client confirms that it can do MSDP.
|
||||
"""
|
||||
self.protocol.protocol_flags['MSDP'] = True
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def evennia_to_msdp(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
handle return data from cmdname by converting it to
|
||||
a proper msdp structure. data can either be a single value (will be
|
||||
converted to a string), a list (will be converted to an MSDP_ARRAY),
|
||||
or a dictionary (will be converted to MSDP_TABLE).
|
||||
|
||||
OBS - there is no actual use of arrays and tables in the MSDP
|
||||
specification or default commands -- are returns are implemented
|
||||
as simple lists or named lists (our name for them here, these
|
||||
un-bounded structures are not named in the specification). So for
|
||||
now, this routine will not explicitly create arrays nor tables,
|
||||
although there are helper methods ready should it be needed in
|
||||
the future.
|
||||
"""
|
||||
|
||||
def make_table(name, **kwargs):
|
||||
"build a table that may be nested with other tables or arrays."
|
||||
string = MSDP_VAR + force_str(name) + MSDP_VAL + MSDP_TABLE_OPEN
|
||||
for key, val in kwargs.items():
|
||||
if isinstance(val, dict):
|
||||
string += make_table(string, key, **val)
|
||||
elif hasattr(val, '__iter__'):
|
||||
string += make_array(string, key, *val)
|
||||
else:
|
||||
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
|
||||
string += MSDP_TABLE_CLOSE
|
||||
return string
|
||||
|
||||
def make_array(name, *args):
|
||||
"build a array. Arrays may not nest tables by definition."
|
||||
string = MSDP_VAR + force_str(name) + MSDP_ARRAY_OPEN
|
||||
string += MSDP_VAL.join(force_str(arg) for arg in args)
|
||||
string += MSDP_ARRAY_CLOSE
|
||||
return string
|
||||
|
||||
def make_list(name, *args):
|
||||
"build a simple list - an array without start/end markers"
|
||||
string = MSDP_VAR + force_str(name)
|
||||
string += MSDP_VAL.join(force_str(arg) for arg in args)
|
||||
return string
|
||||
|
||||
def make_named_list(name, **kwargs):
|
||||
"build a named list - a table without start/end markers"
|
||||
string = MSDP_VAR + force_str(name)
|
||||
for key, val in kwargs.items():
|
||||
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
|
||||
return string
|
||||
|
||||
# Default MSDP commands
|
||||
|
||||
print "MSDP outgoing:", cmdname, args, kwargs
|
||||
|
||||
cupper = cmdname.upper()
|
||||
if cupper == "LIST":
|
||||
if args:
|
||||
args = list(args)
|
||||
mode = args.pop(0).upper()
|
||||
self.data_out(make_array(mode, *args))
|
||||
elif cupper == "REPORT":
|
||||
self.data_out(make_list("REPORT", *args))
|
||||
elif cupper == "UNREPORT":
|
||||
self.data_out(make_list("UNREPORT", *args))
|
||||
elif cupper == "RESET":
|
||||
self.data_out(make_list("RESET", *args))
|
||||
elif cupper == "SEND":
|
||||
self.data_out(make_named_list("SEND", **kwargs))
|
||||
else:
|
||||
# return list or named lists.
|
||||
msdp_string = ""
|
||||
if args:
|
||||
msdp_string += make_list(cupper, *args)
|
||||
if kwargs:
|
||||
msdp_string += make_named_list(cupper, **kwargs)
|
||||
self.data_out(msdp_string)
|
||||
|
||||
def msdp_to_evennia(self, data):
|
||||
"""
|
||||
Handle a client's requested negotiation, converting
|
||||
it into a function mapping - either one of the MSDP
|
||||
default functions (LIST, SEND etc) or a custom one
|
||||
in OOB_FUNCS dictionary. command names are case-insensitive.
|
||||
|
||||
varname, var --> mapped to function varname(var)
|
||||
arrayname, array --> mapped to function arrayname(*array)
|
||||
tablename, table --> mapped to function tablename(**table)
|
||||
|
||||
Note: Combinations of args/kwargs to one function is not supported
|
||||
in this implementation (it complicates the code for limited
|
||||
gain - arrayname(*array) is usually as complex as anyone should
|
||||
ever need to go anyway (I hope!).
|
||||
|
||||
"""
|
||||
tables = {}
|
||||
arrays = {}
|
||||
variables = {}
|
||||
|
||||
if hasattr(data, "__iter__"):
|
||||
data = "".join(data)
|
||||
|
||||
#logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
|
||||
|
||||
for key, table in regex_table.findall(data):
|
||||
tables[key] = {}
|
||||
for varval in regex_var.split(table):
|
||||
parts = regex_val.split(varval)
|
||||
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)})
|
||||
for key, array in regex_array.findall(data):
|
||||
arrays[key] = []
|
||||
for val in regex_val.split(array):
|
||||
arrays[key].append(val)
|
||||
arrays[key] = tuple(arrays[key])
|
||||
for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))):
|
||||
# get remaining varvals after cleaning away tables/arrays
|
||||
parts = regex_val.split(varval)
|
||||
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", )
|
||||
|
||||
#print "MSDP: table, array, variables:", tables, arrays, variables
|
||||
|
||||
# all variables sent through msdp to Evennia are considered commands
|
||||
# with arguments. There are three forms of commands possible
|
||||
# through msdp:
|
||||
#
|
||||
# VARNAME VAR -> varname(var)
|
||||
# ARRAYNAME VAR VAL VAR VAL VAR VAL ENDARRAY -> arrayname(val,val,val)
|
||||
# TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE ->
|
||||
# tablename(varname=val, varname=val)
|
||||
#
|
||||
|
||||
# default MSDP functions
|
||||
if "LIST" in variables:
|
||||
self.data_in("list", *variables.pop("LIST"))
|
||||
if "REPORT" in variables:
|
||||
self.data_in("report", *variables.pop("REPORT"))
|
||||
if "REPORT" in arrays:
|
||||
self.data_in("report", *(arrays.pop("REPORT")))
|
||||
if "UNREPORT" in variables:
|
||||
self.data_in("unreport", *(arrays.pop("UNREPORT")))
|
||||
if "RESET" in variables:
|
||||
self.data_in("reset", *variables.pop("RESET"))
|
||||
if "RESET" in arrays:
|
||||
self.data_in("reset", *(arrays.pop("RESET")))
|
||||
if "SEND" in variables:
|
||||
self.data_in("send", *variables.pop("SEND"))
|
||||
if "SEND" in arrays:
|
||||
self.data_in("send", *(arrays.pop("SEND")))
|
||||
|
||||
# if there are anything left consider it a call to a custom function
|
||||
|
||||
for varname, var in variables.items():
|
||||
# a simple function + argument
|
||||
self.data_in(varname, (var,))
|
||||
for arrayname, array in arrays.items():
|
||||
# we assume the array are multiple arguments to the function
|
||||
self.data_in(arrayname, *array)
|
||||
for tablename, table in tables.items():
|
||||
# we assume tables are keyword arguments to the function
|
||||
self.data_in(tablename, **table)
|
||||
|
||||
def data_out(self, msdp_string):
|
||||
"""
|
||||
Return a msdp-valid subnegotiation across the protocol.
|
||||
"""
|
||||
#print "msdp data_out (without IAC SE):", msdp_string
|
||||
self.protocol ._write(IAC + SB + MSDP + force_str(msdp_string) + IAC + SE)
|
||||
|
||||
def data_in(self, funcname, *args, **kwargs):
|
||||
"""
|
||||
Send oob data to Evennia
|
||||
"""
|
||||
#print "msdp data_in:", funcname, args, kwargs
|
||||
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))
|
||||
185
lib/server/portal/mssp.py
Normal file
185
lib/server/portal/mssp.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
"""
|
||||
|
||||
MSSP - Mud Server Status Protocol
|
||||
|
||||
This implements the MSSP telnet protocol as per
|
||||
http://tintin.sourceforge.net/mssp/. MSSP allows web portals and
|
||||
listings to have their crawlers find the mud and automatically
|
||||
extract relevant information about it, such as genre, how many
|
||||
active players and so on.
|
||||
|
||||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
|
||||
MSSP = chr(70)
|
||||
MSSP_VAR = chr(1)
|
||||
MSSP_VAL = chr(2)
|
||||
|
||||
|
||||
# try to get the customized mssp info, if it exists.
|
||||
MSSPTable_CUSTOM = utils.variable_from_module(settings.MSSP_META_MODULE, "MSSPTable", default={})
|
||||
|
||||
class Mssp(object):
|
||||
"""
|
||||
Implements the MSSP protocol. Add this to a
|
||||
variable on the telnet protocol to set it up.
|
||||
"""
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
initialize MSSP by storing protocol on ourselves
|
||||
and calling the client to see if it supports
|
||||
MSSP.
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.will(MSSP).addCallbacks(self.do_mssp, self.no_mssp)
|
||||
|
||||
def get_player_count(self):
|
||||
"Get number of logged-in players"
|
||||
return str(self.protocol.sessionhandler.count_loggedin())
|
||||
|
||||
def get_uptime(self):
|
||||
"Get how long the portal has been online (reloads are not counted)"
|
||||
return str(self.protocol.sessionhandler.uptime)
|
||||
|
||||
def no_mssp(self, option):
|
||||
"""
|
||||
This is the normal operation.
|
||||
"""
|
||||
self.protocol.handshake_done()
|
||||
pass
|
||||
|
||||
def do_mssp(self, option):
|
||||
"""
|
||||
Negotiate all the information.
|
||||
"""
|
||||
|
||||
self.mssp_table = {
|
||||
|
||||
# Required fields
|
||||
|
||||
"NAME": "Evennia",
|
||||
"PLAYERS": self.get_player_count,
|
||||
"UPTIME" : self.get_uptime,
|
||||
|
||||
# Generic
|
||||
|
||||
"CRAWL DELAY": "-1",
|
||||
|
||||
"HOSTNAME": "", # current or new hostname
|
||||
"PORT": ["4000"], # most important port should be last in list
|
||||
"CODEBASE": "Evennia",
|
||||
"CONTACT": "", # email for contacting the mud
|
||||
"CREATED": "", # year MUD was created
|
||||
"ICON": "", # url to icon 32x32 or larger; <32kb.
|
||||
"IP": "", # current or new IP address
|
||||
"LANGUAGE": "", # name of language used, e.g. English
|
||||
"LOCATION": "", # full English name of server country
|
||||
"MINIMUM AGE": "0", # set to 0 if not applicable
|
||||
"WEBSITE": "www.evennia.com",
|
||||
|
||||
# Categorisation
|
||||
|
||||
"FAMILY": "Custom", # evennia goes under 'Custom'
|
||||
"GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
|
||||
"GAMEPLAY": "None", # Adventure, Educational, Hack and Slash, None,
|
||||
# Player versus Player, Player versus Environment,
|
||||
# Roleplaying, Simulation, Social or Strategy
|
||||
"STATUS": "Open Beta", # Alpha, Closed Beta, Open Beta, Live
|
||||
"GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew
|
||||
"INTERMUD": "IMC2", # evennia supports IMC2.
|
||||
"SUBGENRE": "None", # LASG, Medieval Fantasy, World War II, Frankenstein,
|
||||
# Cyberpunk, Dragonlance, etc. Or None if not available.
|
||||
|
||||
# World
|
||||
|
||||
"AREAS": "0",
|
||||
"HELPFILES": "0",
|
||||
"MOBILES": "0",
|
||||
"OBJECTS": "0",
|
||||
"ROOMS": "0", # use 0 if room-less
|
||||
"CLASSES": "0", # use 0 if class-less
|
||||
"LEVELS": "0", # use 0 if level-less
|
||||
"RACES": "0", # use 0 if race-less
|
||||
"SKILLS": "0", # use 0 if skill-less
|
||||
|
||||
# Protocols set to 1 or 0)
|
||||
|
||||
"ANSI": "1",
|
||||
"GMCP": "0",
|
||||
"MCCP": "0",
|
||||
"MCP": "0",
|
||||
"MSDP": "0",
|
||||
"MSP": "0",
|
||||
"MXP": "0",
|
||||
"PUEBLO": "0",
|
||||
"UTF-8": "1",
|
||||
"VT100": "0",
|
||||
"XTERM 256 COLORS": "0",
|
||||
|
||||
# Commercial set to 1 or 0)
|
||||
|
||||
"PAY TO PLAY": "0",
|
||||
"PAY FOR PERKS": "0",
|
||||
|
||||
# Hiring set to 1 or 0)
|
||||
|
||||
"HIRING BUILDERS": "0",
|
||||
"HIRING CODERS": "0",
|
||||
|
||||
# Extended variables
|
||||
|
||||
# World
|
||||
|
||||
"DBSIZE": "0",
|
||||
"EXITS": "0",
|
||||
"EXTRA DESCRIPTIONS": "0",
|
||||
"MUDPROGS": "0",
|
||||
"MUDTRIGS": "0",
|
||||
"RESETS": "0",
|
||||
|
||||
# Game (set to 1, 0 or one of the given alternatives)
|
||||
|
||||
"ADULT MATERIAL": "0",
|
||||
"MULTICLASSING": "0",
|
||||
"NEWBIE FRIENDLY": "0",
|
||||
"PLAYER CITIES": "0",
|
||||
"PLAYER CLANS": "0",
|
||||
"PLAYER CRAFTING": "0",
|
||||
"PLAYER GUILDS": "0",
|
||||
"EQUIPMENT SYSTEM": "None", # "None", "Level", "Skill", "Both"
|
||||
"MULTIPLAYING": "None", # "None", "Restricted", "Full"
|
||||
"PLAYERKILLING": "None", # "None", "Restricted", "Full"
|
||||
"QUEST SYSTEM": "None", # "None", "Immortal Run", "Automated", "Integrated"
|
||||
"ROLEPLAYING": "None", # "None", "Accepted", "Encouraged", "Enforced"
|
||||
"TRAINING SYSTEM": "None", # "None", "Level", "Skill", "Both"
|
||||
"WORLD ORIGINALITY": "None", # "All Stock", "Mostly Stock", "Mostly Original", "All Original"
|
||||
|
||||
# Protocols (only change if you added/removed something manually)
|
||||
|
||||
"ATCP": "0",
|
||||
"MSDP": "0",
|
||||
"MCCP": "1",
|
||||
"SSL": "1",
|
||||
"UTF-8": "1",
|
||||
"ZMP": "0",
|
||||
"XTERM 256 COLORS": "0"}
|
||||
|
||||
# update the static table with the custom one
|
||||
if MSSPTable_CUSTOM:
|
||||
self.mssp_table.update(MSSPTable_CUSTOM)
|
||||
|
||||
varlist = ''
|
||||
for variable, value in self.mssp_table.items():
|
||||
if callable(value):
|
||||
value = value()
|
||||
if utils.is_iter(value):
|
||||
for partval in value:
|
||||
varlist += MSSP_VAR + str(variable) + MSSP_VAL + str(partval)
|
||||
else:
|
||||
varlist += MSSP_VAR + str(variable) + MSSP_VAL + str(value)
|
||||
|
||||
# send to crawler by subnegotiation
|
||||
self.protocol.requestNegotiation(MSSP, varlist)
|
||||
self.protocol.handshake_done()
|
||||
62
lib/server/portal/mxp.py
Normal file
62
lib/server/portal/mxp.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""
|
||||
MXP - Mud eXtension Protocol.
|
||||
|
||||
Partial implementation of the MXP protocol.
|
||||
The MXP protocol allows more advanced formatting options for telnet clients
|
||||
that supports it (mudlet, zmud, mushclient are a few)
|
||||
|
||||
This only implements the SEND tag.
|
||||
|
||||
More information can be found on the following links:
|
||||
http://www.zuggsoft.com/zmud/mxp.htm
|
||||
http://www.mushclient.com/mushclient/mxp.htm
|
||||
http://www.gammon.com.au/mushclient/addingservermxp.htm
|
||||
"""
|
||||
import re
|
||||
|
||||
LINKS_SUB = re.compile(r'\{lc(.*?)\{lt(.*?)\{le', re.DOTALL)
|
||||
|
||||
MXP = "\x5B"
|
||||
MXP_TEMPSECURE = "\x1B[4z"
|
||||
MXP_SEND = MXP_TEMPSECURE + \
|
||||
"<SEND HREF='\\1'>" + \
|
||||
"\\2" + \
|
||||
MXP_TEMPSECURE + \
|
||||
"</SEND>"
|
||||
|
||||
def mxp_parse(text):
|
||||
"""
|
||||
Replaces links to the correct format for MXP.
|
||||
"""
|
||||
text = text.replace("&", "&") \
|
||||
.replace("<", "<") \
|
||||
.replace(">", ">")
|
||||
|
||||
text = LINKS_SUB.sub(MXP_SEND, text)
|
||||
return text
|
||||
|
||||
class Mxp(object):
|
||||
"""
|
||||
Implements the MXP protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
"""Initializes the protocol by checking if the client supports it."""
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags["MXP"] = False
|
||||
self.protocol.will(MXP).addCallbacks(self.do_mxp, self.no_mxp)
|
||||
|
||||
def no_mxp(self, option):
|
||||
"""
|
||||
Client does not support MXP.
|
||||
"""
|
||||
self.protocol.protocol_flags["MXP"] = False
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_mxp(self, option):
|
||||
"""
|
||||
Client does support MXP.
|
||||
"""
|
||||
self.protocol.protocol_flags["MXP"] = True
|
||||
self.protocol.handshake_done()
|
||||
self.protocol.requestNegotiation(MXP, '')
|
||||
61
lib/server/portal/naws.py
Normal file
61
lib/server/portal/naws.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""
|
||||
|
||||
NAWS - Negotiate About Window Size
|
||||
|
||||
This implements the NAWS telnet option as per
|
||||
https://www.ietf.org/rfc/rfc1073.txt
|
||||
|
||||
NAWS allows telnet clients to report their
|
||||
current window size to the client and update
|
||||
it when the size changes
|
||||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from src.utils import utils
|
||||
|
||||
NAWS = chr(31)
|
||||
IS = chr(0)
|
||||
# default taken from telnet specification
|
||||
DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
DEFAULT_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT
|
||||
|
||||
# try to get the customized mssp info, if it exists.
|
||||
|
||||
class Naws(object):
|
||||
"""
|
||||
Implements the MSSP protocol. Add this to a
|
||||
variable on the telnet protocol to set it up.
|
||||
"""
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
initialize NAWS by storing protocol on ourselves
|
||||
and calling the client to see if it supports
|
||||
NAWS.
|
||||
"""
|
||||
self.naws_step = 0
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags['SCREENWIDTH'] = {0: DEFAULT_WIDTH} # windowID (0 is root):width
|
||||
self.protocol.protocol_flags['SCREENHEIGHT'] = {0: DEFAULT_HEIGHT} # windowID:width
|
||||
self.protocol.negotiationMap[NAWS] = self.negotiate_sizes
|
||||
self.protocol.do(NAWS).addCallbacks(self.do_naws, self.no_naws)
|
||||
|
||||
def no_naws(self, option):
|
||||
"""
|
||||
This is the normal operation.
|
||||
"""
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_naws(self, option):
|
||||
"""
|
||||
Negotiate all the information.
|
||||
"""
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def negotiate_sizes(self, options):
|
||||
if len(options) == 4:
|
||||
# NAWS is negotiated with 16bit words
|
||||
width = options[0] + options[1]
|
||||
self.protocol.protocol_flags['SCREENWIDTH'][0] = int(width.encode('hex'), 16)
|
||||
height = options[2] + options[3]
|
||||
self.protocol.protocol_flags['SCREENHEIGHT'][0] = int(height.encode('hex'), 16)
|
||||
|
||||
312
lib/server/portal/portal.py
Normal file
312
lib/server/portal/portal.py
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
"""
|
||||
This module implements the main Evennia server process, the core of
|
||||
the game engine.
|
||||
|
||||
This module should be started with the 'twistd' executable since it
|
||||
sets up all the networking features. (this is done automatically
|
||||
by game/evennia.py).
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
if os.name == 'nt':
|
||||
# For Windows batchfile we need an extra path insertion here.
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__))))))
|
||||
from src.server.webserver import EvenniaReverseProxyResource
|
||||
from twisted.application import internet, service
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.web import server
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
from django.conf import settings
|
||||
from src.utils.utils import get_evennia_version, mod_import, make_iter
|
||||
from src.server.portal.portalsessionhandler import PORTAL_SESSIONS
|
||||
|
||||
PORTAL_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)]
|
||||
|
||||
if os.name == 'nt':
|
||||
# For Windows we need to handle pid files manually.
|
||||
PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, 'portal.pid')
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Evennia Portal settings
|
||||
#------------------------------------------------------------
|
||||
|
||||
VERSION = get_evennia_version()
|
||||
|
||||
SERVERNAME = settings.SERVERNAME
|
||||
|
||||
PORTAL_RESTART = os.path.join(settings.GAME_DIR, 'portal.restart')
|
||||
|
||||
TELNET_PORTS = settings.TELNET_PORTS
|
||||
SSL_PORTS = settings.SSL_PORTS
|
||||
SSH_PORTS = settings.SSH_PORTS
|
||||
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
|
||||
WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT
|
||||
|
||||
TELNET_INTERFACES = settings.TELNET_INTERFACES
|
||||
SSL_INTERFACES = settings.SSL_INTERFACES
|
||||
SSH_INTERFACES = settings.SSH_INTERFACES
|
||||
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
|
||||
WEBSOCKET_CLIENT_INTERFACE = settings.WEBSOCKET_CLIENT_INTERFACE
|
||||
WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL
|
||||
|
||||
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
|
||||
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
|
||||
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
|
||||
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
|
||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||
WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE
|
||||
|
||||
AMP_HOST = settings.AMP_HOST
|
||||
AMP_PORT = settings.AMP_PORT
|
||||
AMP_INTERFACE = settings.AMP_INTERFACE
|
||||
AMP_ENABLED = AMP_HOST and AMP_PORT and AMP_INTERFACE
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Portal Service object
|
||||
#------------------------------------------------------------
|
||||
class Portal(object):
|
||||
|
||||
"""
|
||||
The main Portal server handler. This object sets up the database and
|
||||
tracks and interlinks all the twisted network services that make up
|
||||
Portal.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
"""
|
||||
Setup the server.
|
||||
|
||||
application - an instantiated Twisted application
|
||||
|
||||
"""
|
||||
sys.path.append('.')
|
||||
|
||||
# create a store of services
|
||||
self.services = service.IServiceCollection(application)
|
||||
self.amp_protocol = None # set by amp factory
|
||||
self.sessions = PORTAL_SESSIONS
|
||||
self.sessions.portal = self
|
||||
|
||||
# set a callback if the server is killed abruptly,
|
||||
# by Ctrl-C, reboot etc.
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True)
|
||||
|
||||
self.game_running = False
|
||||
|
||||
def set_restart_mode(self, mode=None):
|
||||
"""
|
||||
This manages the flag file that tells the runner if the server should
|
||||
be restarted or is shutting down. Valid modes are True/False and None.
|
||||
If mode is None, no change will be done to the flag file.
|
||||
"""
|
||||
if mode is None:
|
||||
return
|
||||
f = open(PORTAL_RESTART, 'w')
|
||||
print "writing mode=%(mode)s to %(portal_restart)s" % {'mode': mode, 'portal_restart': PORTAL_RESTART}
|
||||
f.write(str(mode))
|
||||
f.close()
|
||||
|
||||
def shutdown(self, restart=None, _reactor_stopping=False):
|
||||
"""
|
||||
Shuts down the server from inside it.
|
||||
|
||||
restart - True/False sets the flags so the server will be
|
||||
restarted or not. If None, the current flag setting
|
||||
(set at initialization or previous runs) is used.
|
||||
_reactor_stopping - this is set if server is already in the process of
|
||||
shutting down; in this case we don't need to stop it again.
|
||||
|
||||
Note that restarting (regardless of the setting) will not work
|
||||
if the Portal is currently running in daemon mode. In that
|
||||
case it always needs to be restarted manually.
|
||||
"""
|
||||
if _reactor_stopping and hasattr(self, "shutdown_complete"):
|
||||
# we get here due to us calling reactor.stop below. No need
|
||||
# to do the shutdown procedure again.
|
||||
return
|
||||
self.set_restart_mode(restart)
|
||||
if os.name == 'nt' and os.path.exists(PORTAL_PIDFILE):
|
||||
# for Windows we need to remove pid files manually
|
||||
os.remove(PORTAL_PIDFILE)
|
||||
if not _reactor_stopping:
|
||||
# shutting down the reactor will trigger another signal. We set
|
||||
# a flag to avoid loops.
|
||||
self.shutdown_complete = True
|
||||
reactor.callLater(0, reactor.stop)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Start the Portal proxy server and add all active services
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
# twistd requires us to define the variable 'application' so it knows
|
||||
# what to execute from.
|
||||
application = service.Application('Portal')
|
||||
|
||||
# The main Portal server program. This sets up the database
|
||||
# and is where we store all the other services.
|
||||
PORTAL = Portal(application)
|
||||
|
||||
print '-' * 50
|
||||
print ' %(servername)s Portal (%(version)s) started.' % {'servername': SERVERNAME, 'version': VERSION}
|
||||
|
||||
if AMP_ENABLED:
|
||||
|
||||
# The AMP protocol handles the communication between
|
||||
# the portal and the mud server. Only reason to ever deactivate
|
||||
# it would be during testing and debugging.
|
||||
|
||||
from src.server import amp
|
||||
|
||||
print ' amp (to Server): %s' % AMP_PORT
|
||||
|
||||
factory = amp.AmpClientFactory(PORTAL)
|
||||
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
|
||||
amp_client.setName('evennia_amp')
|
||||
PORTAL.services.addService(amp_client)
|
||||
|
||||
|
||||
# We group all the various services under the same twisted app.
|
||||
# These will gradually be started as they are initialized below.
|
||||
|
||||
if TELNET_ENABLED:
|
||||
|
||||
# Start telnet game connections
|
||||
|
||||
from src.server.portal import telnet
|
||||
|
||||
for interface in TELNET_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface not in ('0.0.0.0', '::') or len(TELNET_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in TELNET_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = telnet.TelnetProtocol
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
telnet_service = internet.TCPServer(port, factory, interface=interface)
|
||||
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
||||
PORTAL.services.addService(telnet_service)
|
||||
|
||||
print ' telnet%s: %s' % (ifacestr, port)
|
||||
|
||||
|
||||
if SSL_ENABLED:
|
||||
|
||||
# Start SSL game connection (requires PyOpenSSL).
|
||||
|
||||
from src.server.portal import ssl
|
||||
|
||||
for interface in SSL_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface not in ('0.0.0.0', '::') or len(SSL_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSL_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
factory.protocol = ssl.SSLProtocol
|
||||
ssl_service = internet.SSLServer(port,
|
||||
factory,
|
||||
ssl.getSSLContext(),
|
||||
interface=interface)
|
||||
ssl_service.setName('EvenniaSSL%s' % pstring)
|
||||
PORTAL.services.addService(ssl_service)
|
||||
|
||||
print " ssl%s: %s" % (ifacestr, port)
|
||||
|
||||
|
||||
if SSH_ENABLED:
|
||||
|
||||
# Start SSH game connections. Will create a keypair in
|
||||
# evennia/game if necessary.
|
||||
|
||||
from src.server.portal import ssh
|
||||
|
||||
for interface in SSH_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface not in ('0.0.0.0', '::') or len(SSH_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for port in SSH_PORTS:
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = ssh.makeFactory({'protocolFactory': ssh.SshProtocol,
|
||||
'protocolArgs': (),
|
||||
'sessions': PORTAL_SESSIONS})
|
||||
ssh_service = internet.TCPServer(port, factory, interface=interface)
|
||||
ssh_service.setName('EvenniaSSH%s' % pstring)
|
||||
PORTAL.services.addService(ssh_service)
|
||||
|
||||
print " ssl%s: %s" % (ifacestr, port)
|
||||
|
||||
|
||||
if WEBSERVER_ENABLED:
|
||||
|
||||
# Start a reverse proxy to relay data to the Server-side webserver
|
||||
|
||||
websocket_started = False
|
||||
for interface in WEBSERVER_INTERFACES:
|
||||
ifacestr = ""
|
||||
if interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
|
||||
ifacestr = "-%s" % interface
|
||||
for proxyport, serverport in WEBSERVER_PORTS:
|
||||
pstring = "%s:%s<->%s" % (ifacestr, proxyport, serverport)
|
||||
web_root = EvenniaReverseProxyResource('127.0.0.1', serverport, '')
|
||||
webclientstr = ""
|
||||
if WEBCLIENT_ENABLED:
|
||||
# create ajax client processes at /webclientdata
|
||||
from src.server.portal.webclient import WebClient
|
||||
|
||||
webclient = WebClient()
|
||||
webclient.sessionhandler = PORTAL_SESSIONS
|
||||
web_root.putChild("webclientdata", webclient)
|
||||
webclientstr = "\n + client (ajax only)"
|
||||
|
||||
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
|
||||
# start websocket client port for the webclient
|
||||
# we only support one websocket client
|
||||
from src.server.portal import websocket_client
|
||||
from src.utils.txws import WebSocketFactory
|
||||
|
||||
interface = WEBSOCKET_CLIENT_INTERFACE
|
||||
port = WEBSOCKET_CLIENT_PORT
|
||||
ifacestr = ""
|
||||
if interface not in ('0.0.0.0', '::'):
|
||||
ifacestr = "-%s" % interface
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = websocket_client.WebSocketClient
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
|
||||
websocket_service.setName('EvenniaWebSocket%s' % pstring)
|
||||
PORTAL.services.addService(websocket_service)
|
||||
websocket_started = True
|
||||
webclientstr = webclientstr[:-11] + "(%s:%s)" % (WEBSOCKET_CLIENT_URL, port)
|
||||
|
||||
web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
proxy_service = internet.TCPServer(proxyport,
|
||||
web_root,
|
||||
interface=interface)
|
||||
proxy_service.setName('EvenniaWebProxy%s' % pstring)
|
||||
PORTAL.services.addService(proxy_service)
|
||||
print " webproxy%s:%s (<-> %s)%s" % (ifacestr, proxyport, serverport, webclientstr)
|
||||
|
||||
|
||||
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
|
||||
# external plugin services to start
|
||||
plugin_module.start_plugin_services(PORTAL)
|
||||
|
||||
print '-' * 50 # end of terminal output
|
||||
|
||||
if os.name == 'nt':
|
||||
# Windows only: Set PID file manually
|
||||
f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w')
|
||||
f.write(str(os.getpid()))
|
||||
f.close()
|
||||
208
lib/server/portal/portalsessionhandler.py
Normal file
208
lib/server/portal/portalsessionhandler.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
"""
|
||||
Sessionhandler for portal sessions
|
||||
"""
|
||||
import time
|
||||
from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN, PSYNC, PCONNSYNC
|
||||
|
||||
_MOD_IMPORT = None
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Portal-SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
class PortalSessionHandler(SessionHandler):
|
||||
"""
|
||||
This object holds the sessions connected to the portal at any time.
|
||||
It is synced with the server's equivalent SessionHandler over the AMP
|
||||
connection.
|
||||
|
||||
Sessions register with the handler using the connect() method. This
|
||||
will assign a new unique sessionid to the session and send that sessid
|
||||
to the server using the AMP connection.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Init the handler
|
||||
"""
|
||||
self.portal = None
|
||||
self.sessions = {}
|
||||
self.latest_sessid = 0
|
||||
self.uptime = time.time()
|
||||
self.connection_time = 0
|
||||
|
||||
def at_server_connection(self):
|
||||
"""
|
||||
Called when the Portal establishes connection with the
|
||||
Server. At this point, the AMP connection is already
|
||||
established.
|
||||
"""
|
||||
self.connection_time = time.time()
|
||||
|
||||
def connect(self, session):
|
||||
"""
|
||||
Called by protocol at first connect. This adds a not-yet
|
||||
authenticated session using an ever-increasing counter for sessid.
|
||||
"""
|
||||
self.latest_sessid += 1
|
||||
sessid = self.latest_sessid
|
||||
session.sessid = sessid
|
||||
sessdata = session.get_sync_data()
|
||||
self.sessions[sessid] = session
|
||||
# sync with server-side
|
||||
if self.portal.amp_protocol: # this is a timing issue
|
||||
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
|
||||
operation=PCONN,
|
||||
data=sessdata)
|
||||
def sync(self, session):
|
||||
"""
|
||||
Called by the protocol of an already connected session. This
|
||||
can be used to sync the session info in a delayed manner,
|
||||
such as when negotiation and handshakes are delayed.
|
||||
"""
|
||||
if session.sessid:
|
||||
# only use if session already has sessid (i.e. has already connected)
|
||||
sessdata = session.get_sync_data()
|
||||
if self.portal.amp_protocol:
|
||||
# we only send sessdata that should not have changed
|
||||
# at the server level at this point
|
||||
sessdata = dict((key, val) for key, val in sessdata.items() if key in ("protocol_key",
|
||||
"address",
|
||||
"sessid",
|
||||
"suid",
|
||||
"conn_time",
|
||||
"protocol_flags",
|
||||
"server_data",))
|
||||
self.portal.amp_protocol.call_remote_ServerAdmin(session.sessid,
|
||||
operation=PCONNSYNC,
|
||||
data=sessdata)
|
||||
|
||||
def disconnect(self, session):
|
||||
"""
|
||||
Called from portal side when the connection is closed
|
||||
from the portal side.
|
||||
"""
|
||||
sessid = session.sessid
|
||||
if sessid in self.sessions:
|
||||
del self.sessions[sessid]
|
||||
del session
|
||||
# tell server to also delete this session
|
||||
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
|
||||
operation=PDISCONN)
|
||||
|
||||
|
||||
def server_connect(self, protocol_path="", config=dict()):
|
||||
"""
|
||||
Called by server to force the initialization of a new
|
||||
protocol instance. Server wants this instance to get
|
||||
a unique sessid and to be connected back as normal. This
|
||||
is used to initiate irc/imc2/rss etc connections.
|
||||
|
||||
protocol_path - full python path to the class factory
|
||||
for the protocol used, eg
|
||||
'src.server.portal.irc.IRCClientFactory'
|
||||
config - dictionary of configuration options, fed as **kwarg
|
||||
to protocol class' __init__ method.
|
||||
|
||||
The called protocol class must have a method start()
|
||||
that calls the portalsession.connect() as a normal protocol.
|
||||
"""
|
||||
global _MOD_IMPORT
|
||||
if not _MOD_IMPORT:
|
||||
from src.utils.utils import variable_from_module as _MOD_IMPORT
|
||||
path, clsname = protocol_path.rsplit(".", 1)
|
||||
cls = _MOD_IMPORT(path, clsname)
|
||||
if not cls:
|
||||
raise RuntimeError("ServerConnect: protocol factory '%s' not found." % protocol_path)
|
||||
protocol = cls(self, **config)
|
||||
protocol.start()
|
||||
|
||||
def server_disconnect(self, sessid, reason=""):
|
||||
"""
|
||||
Called by server to force a disconnect by sessid
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.disconnect(reason)
|
||||
if sessid in self.sessions:
|
||||
# in case sess.disconnect doesn't delete it
|
||||
del self.sessions[sessid]
|
||||
del session
|
||||
|
||||
def server_disconnect_all(self, reason=""):
|
||||
"""
|
||||
Called by server when forcing a clean disconnect for everyone.
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.disconnect(reason)
|
||||
del session
|
||||
self.sessions = {}
|
||||
|
||||
def server_logged_in(self, sessid, data):
|
||||
"""
|
||||
The server tells us that the session has been
|
||||
authenticated. Updated it.
|
||||
"""
|
||||
sess = self.get_session(sessid)
|
||||
sess.load_sync_data(data)
|
||||
|
||||
def server_session_sync(self, serversessions):
|
||||
"""
|
||||
Server wants to save data to the portal, maybe because it's about
|
||||
to shut down. We don't overwrite any sessions here, just update
|
||||
them in-place and remove any that are out of sync (which should
|
||||
normally not be the case)
|
||||
|
||||
serversessions - dictionary {sessid:{property:value},...} describing
|
||||
the properties to sync on all sessions
|
||||
"""
|
||||
to_save = [sessid for sessid in serversessions if sessid in self.sessions]
|
||||
to_delete = [sessid for sessid in self.sessions if sessid not in to_save]
|
||||
# save protocols
|
||||
for sessid in to_save:
|
||||
self.sessions[sessid].load_sync_data(serversessions[sessid])
|
||||
# disconnect out-of-sync missing protocols
|
||||
for sessid in to_delete:
|
||||
self.server_disconnect(sessid)
|
||||
|
||||
def count_loggedin(self, include_unloggedin=False):
|
||||
"""
|
||||
Count loggedin connections, alternatively count all connections.
|
||||
"""
|
||||
return len(self.get_sessions(include_unloggedin=include_unloggedin))
|
||||
|
||||
def session_from_suid(self, suid):
|
||||
"""
|
||||
Given a session id, retrieve the session (this is primarily
|
||||
intended to be called by web clients)
|
||||
"""
|
||||
return [sess for sess in self.get_sessions(include_unloggedin=True)
|
||||
if hasattr(sess, 'suid') and sess.suid == suid]
|
||||
|
||||
def data_in(self, session, text="", **kwargs):
|
||||
"""
|
||||
Called by portal sessions for relaying data coming
|
||||
in from the protocol to the server. data is
|
||||
serialized before passed on.
|
||||
"""
|
||||
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
|
||||
msg=text,
|
||||
data=kwargs)
|
||||
|
||||
def announce_all(self, message):
|
||||
"""
|
||||
Send message to all connection sessions
|
||||
"""
|
||||
for session in self.sessions.values():
|
||||
session.data_out(message)
|
||||
|
||||
def data_out(self, sessid, text=None, **kwargs):
|
||||
"""
|
||||
Called by server for having the portal relay messages and data
|
||||
to the correct session protocol.
|
||||
"""
|
||||
session = self.sessions.get(sessid, None)
|
||||
if session:
|
||||
session.data_out(text=text, **kwargs)
|
||||
|
||||
PORTAL_SESSIONS = PortalSessionHandler()
|
||||
100
lib/server/portal/rss.py
Normal file
100
lib/server/portal/rss.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
RSS parser for Evennia
|
||||
|
||||
This connects an RSS feed to an in-game Evennia channel, sending messages
|
||||
to the channel whenever the feed updates.
|
||||
|
||||
"""
|
||||
|
||||
from twisted.internet import task, threads
|
||||
from django.conf import settings
|
||||
from src.server.session import Session
|
||||
from src.utils import logger
|
||||
|
||||
RSS_ENABLED = settings.RSS_ENABLED
|
||||
#RETAG = re.compile(r'<[^>]*?>')
|
||||
|
||||
if RSS_ENABLED:
|
||||
try:
|
||||
import feedparser
|
||||
except ImportError:
|
||||
raise ImportError("RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False.")
|
||||
|
||||
class RSSReader(Session):
|
||||
"""
|
||||
A simple RSS reader using universal feedparser
|
||||
"""
|
||||
def __init__(self, factory, url, rate):
|
||||
self.url = url
|
||||
self.rate = rate
|
||||
self.factory = factory
|
||||
self.old_entries = {}
|
||||
|
||||
def get_new(self):
|
||||
"""Returns list of new items."""
|
||||
feed = feedparser.parse(self.url)
|
||||
new_entries = []
|
||||
for entry in feed['entries']:
|
||||
idval = entry['id'] + entry.get("updated", "")
|
||||
if idval not in self.old_entries:
|
||||
self.old_entries[idval] = entry
|
||||
new_entries.append(entry)
|
||||
return new_entries
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"Disconnect from feed"
|
||||
if self.factory.task and self.factory.task.running:
|
||||
self.factory.task.stop()
|
||||
self.sessionhandler.disconnect(self)
|
||||
|
||||
def _callback(self, new_entries, init):
|
||||
"Called when RSS returns (threaded)"
|
||||
if not init:
|
||||
# for initialization we just ignore old entries
|
||||
for entry in reversed(new_entries):
|
||||
self.data_in("bot_data_in " + entry)
|
||||
|
||||
def data_in(self, text=None, **kwargs):
|
||||
"Data RSS -> Server"
|
||||
self.sessionhandler.data_in(self, text=text, **kwargs)
|
||||
|
||||
def _errback(self, fail):
|
||||
"Report error"
|
||||
logger.log_errmsg("RSS feed error: %s" % fail.value)
|
||||
|
||||
def update(self, init=False):
|
||||
"Request feed"
|
||||
return threads.deferToThread(self.get_new).addCallback(self._callback, init).addErrback(self._errback)
|
||||
|
||||
class RSSBotFactory(object):
|
||||
"""
|
||||
Initializes new bots
|
||||
"""
|
||||
|
||||
def __init__(self, sessionhandler, uid=None, url=None, rate=None):
|
||||
"Initialize"
|
||||
self.sessionhandler = sessionhandler
|
||||
self.url = url
|
||||
self.rate = rate
|
||||
self.uid = uid
|
||||
self.bot = RSSReader(self, url, rate)
|
||||
self.task = None
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Called by portalsessionhandler
|
||||
"""
|
||||
def errback(fail):
|
||||
logger.log_errmsg(fail.value)
|
||||
|
||||
# set up session and connect it to sessionhandler
|
||||
self.bot.init_session("rssbot", self.url, self.sessionhandler)
|
||||
self.bot.uid = self.uid
|
||||
self.bot.logged_in = True
|
||||
self.sessionhandler.connect(self.bot)
|
||||
|
||||
# start repeater task
|
||||
self.bot.update(init=True)
|
||||
self.task = task.LoopingCall(self.bot.update)
|
||||
if self.rate:
|
||||
self.task.start(self.rate, now=False).addErrback(errback)
|
||||
346
lib/server/portal/ssh.py
Normal file
346
lib/server/portal/ssh.py
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
"""
|
||||
This module implements the ssh (Secure SHell) protocol for encrypted
|
||||
connections.
|
||||
|
||||
This depends on a generic session module that implements
|
||||
the actual login procedure of the game, tracks
|
||||
sessions etc.
|
||||
|
||||
Using standard ssh client,
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
from twisted.cred.checkers import credentials
|
||||
from twisted.cred.portal import Portal
|
||||
from twisted.conch.ssh.keys import Key
|
||||
from twisted.conch.interfaces import IConchUser
|
||||
from twisted.conch.ssh.userauth import SSHUserAuthServer
|
||||
from twisted.conch.ssh import common
|
||||
from twisted.conch.insults import insults
|
||||
from twisted.conch.manhole_ssh import TerminalRealm, _Glue, ConchFactory
|
||||
from twisted.conch.manhole import Manhole, recvline
|
||||
from twisted.internet import defer
|
||||
from twisted.conch import interfaces as iconch
|
||||
from twisted.python import components
|
||||
from django.conf import settings
|
||||
from src.server import session
|
||||
from src.players.models import PlayerDB
|
||||
from src.utils import ansi, utils
|
||||
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
||||
CTRL_C = '\x03'
|
||||
CTRL_D = '\x04'
|
||||
CTRL_BACKSLASH = '\x1c'
|
||||
CTRL_L = '\x0c'
|
||||
|
||||
|
||||
class SshProtocol(Manhole, session.Session):
|
||||
"""
|
||||
Each player connecting over ssh gets this protocol assigned to
|
||||
them. All communication between game and player goes through
|
||||
here.
|
||||
"""
|
||||
def __init__(self, starttuple):
|
||||
"""
|
||||
For setting up the player. If player is not None then we'll
|
||||
login automatically.
|
||||
"""
|
||||
self.authenticated_player = starttuple[0]
|
||||
# obs must not be called self.factory, that gets overwritten!
|
||||
self.cfactory = starttuple[1]
|
||||
|
||||
def terminalSize(self, width, height):
|
||||
"""
|
||||
Initialize the terminal and connect to the new session.
|
||||
"""
|
||||
# Clear the previous input line, redraw it at the new
|
||||
# cursor position
|
||||
self.terminal.eraseDisplay()
|
||||
self.terminal.cursorHome()
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
# initialize the session
|
||||
client_address = self.getClientAddress()
|
||||
self.init_session("ssh", client_address, self.cfactory.sessionhandler)
|
||||
|
||||
# since we might have authenticated already, we might set this here.
|
||||
if self.authenticated_player:
|
||||
self.logged_in = True
|
||||
self.uid = self.authenticated_player.user.id
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This is called when the connection is first
|
||||
established.
|
||||
"""
|
||||
recvline.HistoricRecvLine.connectionMade(self)
|
||||
self.keyHandlers[CTRL_C] = self.handle_INT
|
||||
self.keyHandlers[CTRL_D] = self.handle_EOF
|
||||
self.keyHandlers[CTRL_L] = self.handle_FF
|
||||
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
|
||||
|
||||
# initalize
|
||||
|
||||
def handle_INT(self):
|
||||
"""
|
||||
Handle ^C as an interrupt keystroke by resetting the current input
|
||||
variables to their initial state.
|
||||
"""
|
||||
self.lineBuffer = []
|
||||
self.lineBufferIndex = 0
|
||||
|
||||
self.terminal.nextLine()
|
||||
self.terminal.write("KeyboardInterrupt")
|
||||
self.terminal.nextLine()
|
||||
|
||||
def handle_EOF(self):
|
||||
"""
|
||||
Handles EOF generally used to exit.
|
||||
"""
|
||||
if self.lineBuffer:
|
||||
self.terminal.write('\a')
|
||||
else:
|
||||
self.handle_QUIT()
|
||||
|
||||
def handle_FF(self):
|
||||
"""
|
||||
Handle a 'form feed' byte - generally used to request a screen
|
||||
refresh/redraw.
|
||||
"""
|
||||
self.terminal.eraseDisplay()
|
||||
self.terminal.cursorHome()
|
||||
|
||||
def handle_QUIT(self):
|
||||
"""
|
||||
Quit, end, and lose the connection.
|
||||
"""
|
||||
self.terminal.loseConnection()
|
||||
|
||||
def connectionLost(self, reason=None):
|
||||
"""
|
||||
This is executed when the connection is lost for
|
||||
whatever reason. It can also be called directly,
|
||||
from the disconnect method.
|
||||
|
||||
"""
|
||||
insults.TerminalProtocol.connectionLost(self, reason)
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.terminal.loseConnection()
|
||||
|
||||
def getClientAddress(self):
|
||||
"""
|
||||
Returns the client's address and port in a tuple. For example
|
||||
('127.0.0.1', 41917)
|
||||
"""
|
||||
return self.terminal.transport.getPeer()
|
||||
|
||||
def lineReceived(self, string):
|
||||
"""
|
||||
Communication Player -> Evennia. Any line return indicates a
|
||||
command for the purpose of the MUD. So we take the user input
|
||||
and pass it on to the game engine.
|
||||
"""
|
||||
self.sessionhandler.data_in(self, string)
|
||||
|
||||
def lineSend(self, string):
|
||||
"""
|
||||
Communication Evennia -> Player
|
||||
Any string sent should already have been
|
||||
properly formatted and processed
|
||||
before reaching this point.
|
||||
|
||||
"""
|
||||
for line in string.split('\n'):
|
||||
#this is the telnet-specific method for sending
|
||||
self.terminal.write(line)
|
||||
self.terminal.nextLine()
|
||||
|
||||
# session-general method hooks
|
||||
|
||||
def disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
if reason:
|
||||
self.data_out(reason)
|
||||
self.connectionLost(reason)
|
||||
|
||||
def data_out(self, text=None, **kwargs):
|
||||
"""
|
||||
Data Evennia -> Player access hook. 'data' argument is a dict
|
||||
parsed for string settings.
|
||||
|
||||
ssh flags:
|
||||
raw=True - leave all ansi markup and tokens unparsed
|
||||
nomarkup=True - remove all ansi markup
|
||||
|
||||
"""
|
||||
try:
|
||||
text = utils.to_str(text if text else "", encoding=self.encoding)
|
||||
except Exception, e:
|
||||
self.lineSend(str(e))
|
||||
return
|
||||
raw = kwargs.get("raw", False)
|
||||
nomarkup = kwargs.get("nomarkup", False)
|
||||
if raw:
|
||||
self.lineSend(text)
|
||||
else:
|
||||
self.lineSend(ansi.parse_ansi(text.strip("{r") + "{r", strip_ansi=nomarkup))
|
||||
|
||||
|
||||
class ExtraInfoAuthServer(SSHUserAuthServer):
|
||||
def auth_password(self, packet):
|
||||
"""
|
||||
Password authentication.
|
||||
|
||||
Used mostly for setting up the transport so we can query
|
||||
username and password later.
|
||||
"""
|
||||
password = common.getNS(packet[1:])[0]
|
||||
c = credentials.UsernamePassword(self.user, password)
|
||||
c.transport = self.transport
|
||||
return self.portal.login(c, None, IConchUser).addErrback(
|
||||
self._ebPassword)
|
||||
|
||||
|
||||
class PlayerDBPasswordChecker(object):
|
||||
"""
|
||||
Checks the django db for the correct credentials for
|
||||
username/password otherwise it returns the player or None which is
|
||||
useful for the Realm.
|
||||
"""
|
||||
credentialInterfaces = (credentials.IUsernamePassword,)
|
||||
|
||||
def __init__(self, factory):
|
||||
self.factory = factory
|
||||
super(PlayerDBPasswordChecker, self).__init__()
|
||||
|
||||
def requestAvatarId(self, c):
|
||||
"Generic credentials"
|
||||
up = credentials.IUsernamePassword(c, None)
|
||||
username = up.username
|
||||
password = up.password
|
||||
player = PlayerDB.objects.get_player_from_name(username)
|
||||
res = (None, self.factory)
|
||||
if player and player.user.check_password(password):
|
||||
res = (player, self.factory)
|
||||
return defer.succeed(res)
|
||||
|
||||
|
||||
class PassAvatarIdTerminalRealm(TerminalRealm):
|
||||
"""
|
||||
Returns an avatar that passes the avatarId through to the
|
||||
protocol. This is probably not the best way to do it.
|
||||
"""
|
||||
|
||||
def _getAvatar(self, avatarId):
|
||||
comp = components.Componentized()
|
||||
user = self.userFactory(comp, avatarId)
|
||||
sess = self.sessionFactory(comp)
|
||||
|
||||
sess.transportFactory = self.transportFactory
|
||||
sess.chainedProtocolFactory = lambda: self.chainedProtocolFactory(avatarId)
|
||||
|
||||
comp.setComponent(iconch.IConchUser, user)
|
||||
comp.setComponent(iconch.ISession, sess)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class TerminalSessionTransport_getPeer:
|
||||
"""
|
||||
Taken from twisted's TerminalSessionTransport which doesn't
|
||||
provide getPeer to the transport. This one does.
|
||||
"""
|
||||
def __init__(self, proto, chainedProtocol, avatar, width, height):
|
||||
self.proto = proto
|
||||
self.avatar = avatar
|
||||
self.chainedProtocol = chainedProtocol
|
||||
|
||||
session = self.proto.session
|
||||
|
||||
self.proto.makeConnection(
|
||||
_Glue(write=self.chainedProtocol.dataReceived,
|
||||
loseConnection=lambda: avatar.conn.sendClose(session),
|
||||
name="SSH Proto Transport"))
|
||||
|
||||
def loseConnection():
|
||||
self.proto.loseConnection()
|
||||
|
||||
def getPeer():
|
||||
session.conn.transport.transport.getPeer()
|
||||
|
||||
self.chainedProtocol.makeConnection(
|
||||
_Glue(getPeer=getPeer, write=self.proto.write,
|
||||
loseConnection=loseConnection,
|
||||
name="Chained Proto Transport"))
|
||||
|
||||
self.chainedProtocol.terminalProtocol.terminalSize(width, height)
|
||||
|
||||
|
||||
def getKeyPair(pubkeyfile, privkeyfile):
|
||||
"""
|
||||
This function looks for RSA keypair files in the current directory. If they
|
||||
do not exist, the keypair is created.
|
||||
"""
|
||||
|
||||
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
|
||||
# No keypair exists. Generate a new RSA keypair
|
||||
print " Generating SSH RSA keypair ...",
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
KEY_LENGTH = 1024
|
||||
rsaKey = Key(RSA.generate(KEY_LENGTH))
|
||||
publicKeyString = rsaKey.public().toString(type="OPENSSH")
|
||||
privateKeyString = rsaKey.toString(type="OPENSSH")
|
||||
|
||||
# save keys for the future.
|
||||
file(pubkeyfile, 'w+b').write(publicKeyString)
|
||||
file(privkeyfile, 'w+b').write(privateKeyString)
|
||||
print " done."
|
||||
else:
|
||||
publicKeyString = file(pubkeyfile).read()
|
||||
privateKeyString = file(privkeyfile).read()
|
||||
|
||||
return Key.fromString(publicKeyString), Key.fromString(privateKeyString)
|
||||
|
||||
|
||||
def makeFactory(configdict):
|
||||
"""
|
||||
Creates the ssh server factory.
|
||||
"""
|
||||
|
||||
pubkeyfile = "ssh-public.key"
|
||||
privkeyfile = "ssh-private.key"
|
||||
|
||||
def chainProtocolFactory(username=None):
|
||||
return insults.ServerProtocol(
|
||||
configdict['protocolFactory'],
|
||||
*configdict.get('protocolConfigdict', (username,)),
|
||||
**configdict.get('protocolKwArgs', {}))
|
||||
|
||||
rlm = PassAvatarIdTerminalRealm()
|
||||
rlm.transportFactory = TerminalSessionTransport_getPeer
|
||||
rlm.chainedProtocolFactory = chainProtocolFactory
|
||||
factory = ConchFactory(Portal(rlm))
|
||||
factory.sessionhandler = configdict['sessions']
|
||||
|
||||
try:
|
||||
# create/get RSA keypair
|
||||
publicKey, privateKey = getKeyPair(pubkeyfile, privkeyfile)
|
||||
factory.publicKeys = {'ssh-rsa': publicKey}
|
||||
factory.privateKeys = {'ssh-rsa': privateKey}
|
||||
except Exception, e:
|
||||
print " getKeyPair error: %(e)s\n WARNING: Evennia could not auto-generate SSH keypair. Using conch default keys instead." % {'e': e}
|
||||
print " If this error persists, create game/%(pub)s and game/%(priv)s yourself using third-party tools." % {'pub': pubkeyfile, 'priv': privkeyfile}
|
||||
|
||||
factory.services = factory.services.copy()
|
||||
factory.services['ssh-userauth'] = ExtraInfoAuthServer
|
||||
|
||||
factory.portal.registerChecker(PlayerDBPasswordChecker(factory))
|
||||
|
||||
return factory
|
||||
84
lib/server/portal/ssl.py
Normal file
84
lib/server/portal/ssl.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""
|
||||
This is a simple context factory for auto-creating
|
||||
SSL keys and certificates.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from twisted.internet import ssl as twisted_ssl
|
||||
try:
|
||||
import OpenSSL
|
||||
except ImportError:
|
||||
print " SSL_ENABLED requires PyOpenSSL."
|
||||
sys.exit(5)
|
||||
|
||||
from src.server.portal.telnet import TelnetProtocol
|
||||
|
||||
|
||||
class SSLProtocol(TelnetProtocol):
|
||||
"""
|
||||
Communication is the same as telnet, except data transfer
|
||||
is done with encryption.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def verify_SSL_key_and_cert(keyfile, certfile):
|
||||
"""
|
||||
This function looks for RSA key and certificate in the current
|
||||
directory. If files ssl.key and ssl.cert does not exist, they
|
||||
are created.
|
||||
"""
|
||||
|
||||
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
|
||||
# key/cert does not exist. Create.
|
||||
import subprocess
|
||||
from Crypto.PublicKey import RSA
|
||||
from twisted.conch.ssh.keys import Key
|
||||
|
||||
print " Creating SSL key and certificate ... ",
|
||||
|
||||
try:
|
||||
# create the RSA key and store it.
|
||||
KEY_LENGTH = 1024
|
||||
rsaKey = Key(RSA.generate(KEY_LENGTH))
|
||||
keyString = rsaKey.toString(type="OPENSSH")
|
||||
file(keyfile, 'w+b').write(keyString)
|
||||
except Exception, e:
|
||||
print "rsaKey error: %(e)s\n WARNING: Evennia could not auto-generate SSL private key." % {'e': e}
|
||||
print "If this error persists, create game/%(keyfile)s yourself using third-party tools." % {'keyfile': keyfile}
|
||||
sys.exit(5)
|
||||
|
||||
# try to create the certificate
|
||||
CERT_EXPIRE = 365 * 20 # twenty years validity
|
||||
# default:
|
||||
#openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300
|
||||
exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (keyfile, certfile, CERT_EXPIRE)
|
||||
#print "exestring:", exestring
|
||||
try:
|
||||
#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
subprocess.call(exestring)
|
||||
except OSError, e:
|
||||
string = "\n".join([
|
||||
" %s\n" % e,
|
||||
" Evennia's SSL context factory could not automatically",
|
||||
" create an SSL certificate game/%(cert)s." % {'cert': certfile},
|
||||
" A private key 'ssl.key' was already created. Please",
|
||||
" create %(cert)s manually using the commands valid" % {'cert': certfile},
|
||||
" for your operating system.",
|
||||
" Example (linux, using the openssl program): ",
|
||||
" %s" % exestring])
|
||||
print string
|
||||
sys.exit(5)
|
||||
print "done."
|
||||
|
||||
|
||||
def getSSLContext():
|
||||
"""
|
||||
Returns an SSL context (key and certificate). This function
|
||||
verifies that key/cert exists before obtaining the context, and if
|
||||
not, creates them.
|
||||
"""
|
||||
keyfile, certfile = "ssl.key", "ssl.cert"
|
||||
verify_SSL_key_and_cert(keyfile, certfile)
|
||||
return twisted_ssl.DefaultOpenSSLContextFactory(keyfile, certfile)
|
||||
284
lib/server/portal/telnet.py
Normal file
284
lib/server/portal/telnet.py
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
"""
|
||||
This module implements the telnet protocol.
|
||||
|
||||
This depends on a generic session module that implements
|
||||
the actual login procedure of the game, tracks
|
||||
sessions etc.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO
|
||||
from src.server.session import Session
|
||||
from src.server.portal import ttype, mssp, msdp, naws
|
||||
from src.server.portal.mccp import Mccp, mccp_compress, MCCP
|
||||
from src.server.portal.mxp import Mxp, mxp_parse
|
||||
from src.utils import utils, ansi, logger
|
||||
|
||||
_RE_N = re.compile(r"\{n$")
|
||||
_RE_LEND = re.compile(r"\n$|\r$", re.MULTILINE)
|
||||
|
||||
class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||
"""
|
||||
Each player connecting over telnet (ie using most traditional mud
|
||||
clients) gets a telnet protocol instance assigned to them. All
|
||||
communication between game and player goes through here.
|
||||
"""
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This is called when the connection is first
|
||||
established.
|
||||
"""
|
||||
# initialize the session
|
||||
self.iaw_mode = False
|
||||
self.no_lb_mode = False
|
||||
client_address = self.transport.client
|
||||
# this number is counted down for every handshake that completes.
|
||||
# when it reaches 0 the portal/server syncs their data
|
||||
self.handshakes = 6 # naws, ttype, mccp, mssp, msdp, mxp
|
||||
self.init_session("telnet", client_address, self.factory.sessionhandler)
|
||||
|
||||
# negotiate client size
|
||||
self.naws = naws.Naws(self)
|
||||
# negotiate ttype (client info)
|
||||
# Obs: mudlet ttype does not seem to work if we start mccp before ttype. /Griatch
|
||||
self.ttype = ttype.Ttype(self)
|
||||
# negotiate mccp (data compression) - turn this off for wireshark analysis
|
||||
self.mccp = Mccp(self)
|
||||
# negotiate mssp (crawler communication)
|
||||
self.mssp = mssp.Mssp(self)
|
||||
# msdp
|
||||
self.msdp = msdp.Msdp(self)
|
||||
# mxp support
|
||||
self.mxp = Mxp(self)
|
||||
# keepalive watches for dead links
|
||||
self.transport.setTcpKeepAlive(1)
|
||||
# add this new connection to sessionhandler so
|
||||
# the Server becomes aware of it.
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
# timeout the handshakes in case the client doesn't reply at all
|
||||
from src.utils.utils import delay
|
||||
delay(2, callback=self.handshake_done, retval=True)
|
||||
|
||||
def handshake_done(self, force=False):
|
||||
"""
|
||||
This is called by all telnet extensions once they are finished.
|
||||
When all have reported, a sync with the server is performed.
|
||||
The system will force-call this sync after a small time to handle
|
||||
clients that don't reply to handshakes at all.
|
||||
info - debug text from the protocol calling
|
||||
"""
|
||||
if self.handshakes > 0:
|
||||
if force:
|
||||
self.sessionhandler.sync(self)
|
||||
return
|
||||
self.handshakes -= 1
|
||||
if self.handshakes <= 0:
|
||||
# do the sync
|
||||
self.sessionhandler.sync(self)
|
||||
|
||||
def enableRemote(self, option):
|
||||
"""
|
||||
This sets up the remote-activated options we allow for this protocol.
|
||||
"""
|
||||
pass
|
||||
return (option == LINEMODE or
|
||||
option == ttype.TTYPE or
|
||||
option == naws.NAWS or
|
||||
option == MCCP or
|
||||
option == mssp.MSSP)
|
||||
|
||||
def enableLocal(self, option):
|
||||
"""
|
||||
Call to allow the activation of options for this protocol
|
||||
"""
|
||||
return (option == MCCP or option==ECHO)
|
||||
|
||||
def disableLocal(self, option):
|
||||
"""
|
||||
Disable a given option
|
||||
"""
|
||||
if option == ECHO:
|
||||
return True
|
||||
if option == MCCP:
|
||||
self.mccp.no_mccp(option)
|
||||
return True
|
||||
else:
|
||||
return super(TelnetProtocol, self).disableLocal(option)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
this is executed when the connection is lost for
|
||||
whatever reason. it can also be called directly, from
|
||||
the disconnect method
|
||||
"""
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.transport.loseConnection()
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
This method will split the incoming data depending on if it
|
||||
starts with IAC (a telnet command) or not. All other data will
|
||||
be handled in line mode. Some clients also sends an erroneous
|
||||
line break after IAC, which we must watch out for.
|
||||
|
||||
OOB protocols (MSDP etc) already intercept subnegotiations
|
||||
on their own, never entering this method. They will relay
|
||||
their parsed data directly to self.data_in.
|
||||
|
||||
"""
|
||||
|
||||
if data and data[0] == IAC or self.iaw_mode:
|
||||
try:
|
||||
#print "IAC mode"
|
||||
super(TelnetProtocol, self).dataReceived(data)
|
||||
if len(data) == 1:
|
||||
self.iaw_mode = True
|
||||
else:
|
||||
self.iaw_mode = False
|
||||
return
|
||||
except Exception, err1:
|
||||
conv = ""
|
||||
try:
|
||||
for b in data:
|
||||
conv += " " + repr(ord(b))
|
||||
except Exception, err2:
|
||||
conv = str(err2) + ":", str(data)
|
||||
out = "Telnet Error (%s): %s (%s)" % (err1, data, conv)
|
||||
logger.log_trace(out)
|
||||
return
|
||||
|
||||
if self.no_lb_mode and _RE_LEND.match(data):
|
||||
# we are in no_lb_mode and we get a single line break
|
||||
# - this line break should have come with the previous
|
||||
# command - it was already added so we drop it here
|
||||
self.no_lb_mode = False
|
||||
return
|
||||
elif not _RE_LEND.search(data):
|
||||
# no line break at the end of the command, note this.
|
||||
data = data.rstrip("\r\n") + "\n"
|
||||
self.no_lb_mode = True
|
||||
|
||||
# if we get to this point the command should end with a linebreak.
|
||||
# We make sure to add it, to fix some clients messing this up.
|
||||
#print "line data in:", repr(data)
|
||||
StatefulTelnetProtocol.dataReceived(self, data)
|
||||
|
||||
def _write(self, data):
|
||||
"hook overloading the one used in plain telnet"
|
||||
# print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in data))
|
||||
data = data.replace('\n', '\r\n').replace('\r\r\n', '\r\n')
|
||||
#data = data.replace('\n', '\r\n')
|
||||
super(TelnetProtocol, self)._write(mccp_compress(self, data))
|
||||
|
||||
def sendLine(self, line):
|
||||
"hook overloading the one used by linereceiver"
|
||||
#print "sendLine (%s):\n%s" % (self.state, line)
|
||||
#escape IAC in line mode, and correctly add \r\n
|
||||
line += self.delimiter
|
||||
line = line.replace(IAC, IAC + IAC).replace('\n', '\r\n')
|
||||
return self.transport.write(mccp_compress(self, line))
|
||||
|
||||
def lineReceived(self, string):
|
||||
"""
|
||||
Telnet method called when data is coming in over the telnet
|
||||
connection. We pass it on to the game engine directly.
|
||||
"""
|
||||
self.data_in(text=string)
|
||||
|
||||
# Session hooks
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
generic hook for the engine to call in order to
|
||||
disconnect this protocol.
|
||||
"""
|
||||
if reason:
|
||||
self.data_out(reason)
|
||||
self.connectionLost(reason)
|
||||
|
||||
def data_in(self, text=None, **kwargs):
|
||||
"""
|
||||
Data Telnet -> Server
|
||||
"""
|
||||
self.sessionhandler.data_in(self, text=text, **kwargs)
|
||||
|
||||
def data_out(self, text=None, **kwargs):
|
||||
"""
|
||||
Data Evennia -> Player.
|
||||
generic hook method for engine to call in order to send data
|
||||
through the telnet connection.
|
||||
|
||||
valid telnet kwargs:
|
||||
oob=<string> - supply an Out-of-Band instruction.
|
||||
xterm256=True/False - enforce xterm256 setting. If not
|
||||
given, ttype result is used. If
|
||||
client does not suport xterm256, the
|
||||
ansi fallback will be used
|
||||
mxp=True/False - enforce mxp setting. If not given, enables if we
|
||||
detected client support for it
|
||||
ansi=True/False - enforce ansi setting. If not given,
|
||||
ttype result is used.
|
||||
nomarkup=True - strip all ansi markup (this is the same as
|
||||
xterm256=False, ansi=False)
|
||||
raw=True - pass string through without any ansi
|
||||
processing (i.e. include Evennia ansi markers but do
|
||||
not convert them into ansi tokens)
|
||||
prompt=<string> - supply a prompt text which gets sent without a
|
||||
newline added to the end
|
||||
echo=True/False
|
||||
The telnet ttype negotiation flags, if any, are used if no kwargs
|
||||
are given.
|
||||
"""
|
||||
try:
|
||||
text = utils.to_str(text if text else "", encoding=self.encoding)
|
||||
except Exception, e:
|
||||
self.sendLine(str(e))
|
||||
return
|
||||
if "oob" in kwargs:
|
||||
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
|
||||
if "MSDP" in self.protocol_flags:
|
||||
for cmdname, args, kwargs in oobstruct:
|
||||
#print "cmdname, args, kwargs:", cmdname, args, kwargs
|
||||
msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs)
|
||||
#print "msdp_string:", msdp_string
|
||||
self.msdp.data_out(msdp_string)
|
||||
|
||||
# parse **kwargs, falling back to ttype if nothing is given explicitly
|
||||
ttype = self.protocol_flags.get('TTYPE', {})
|
||||
xterm256 = kwargs.get("xterm256", ttype.get('256 COLORS', False) if ttype.get("init_done") else True)
|
||||
useansi = kwargs.get("ansi", ttype and ttype.get('ANSI', False) if ttype.get("init_done") else True)
|
||||
raw = kwargs.get("raw", False)
|
||||
nomarkup = kwargs.get("nomarkup", not (xterm256 or useansi))
|
||||
prompt = kwargs.get("prompt")
|
||||
echo = kwargs.get("echo", None)
|
||||
mxp = kwargs.get("mxp", self.protocol_flags.get("MXP", False))
|
||||
|
||||
#print "telnet kwargs=%s, message=%s" % (kwargs, text)
|
||||
#print "xterm256=%s, useansi=%s, raw=%s, nomarkup=%s, init_done=%s" % (xterm256, useansi, raw, nomarkup, ttype.get("init_done"))
|
||||
if raw:
|
||||
# no processing whatsoever
|
||||
self.sendLine(text)
|
||||
elif text:
|
||||
# we need to make sure to kill the color at the end in order
|
||||
# to match the webclient output.
|
||||
#print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self), "nomarkup: %s, xterm256: %s" % (nomarkup, xterm256)
|
||||
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=xterm256, mxp=mxp)
|
||||
if mxp:
|
||||
linetosend = mxp_parse(linetosend)
|
||||
self.sendLine(linetosend)
|
||||
|
||||
if prompt:
|
||||
# Send prompt separately
|
||||
prompt = ansi.parse_ansi(_RE_N.sub("", prompt) + "{n", strip_ansi=nomarkup, xterm256=xterm256)
|
||||
if mxp:
|
||||
prompt = mxp_parse(prompt)
|
||||
prompt = prompt.replace(IAC, IAC + IAC).replace('\n', '\r\n')
|
||||
prompt += IAC + GA
|
||||
self.transport.write(mccp_compress(self, prompt))
|
||||
if echo:
|
||||
self.transport.write(mccp_compress(self, IAC+WONT+ECHO))
|
||||
elif echo == False:
|
||||
self.transport.write(mccp_compress(self, IAC+WILL+ECHO))
|
||||
|
||||
145
lib/server/portal/ttype.py
Normal file
145
lib/server/portal/ttype.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
"""
|
||||
TTYPE (MTTS) - Mud Terminal Type Standard
|
||||
|
||||
This module implements the TTYPE telnet protocol as per
|
||||
http://tintin.sourceforge.net/mtts/. It allows the server to ask the
|
||||
client about its capabilities. If the client also supports TTYPE, it
|
||||
will return with information such as its name, if it supports colour
|
||||
etc. If the client does not support TTYPE, this will be ignored.
|
||||
|
||||
All data will be stored on the protocol's protocol_flags dictionary,
|
||||
under the 'TTYPE' key.
|
||||
"""
|
||||
|
||||
# telnet option codes
|
||||
TTYPE = chr(24)
|
||||
IS = chr(0)
|
||||
SEND = chr(1)
|
||||
|
||||
# terminal capabilities and their codes
|
||||
MTTS = [(128, 'PROXY'),
|
||||
(64, 'SCREEN READER'),
|
||||
(32, 'OSC COLOR PALETTE'),
|
||||
(16, 'MOUSE TRACKING'),
|
||||
(8, '256 COLORS'),
|
||||
(4, 'UTF-8'),
|
||||
(2, 'VT100'),
|
||||
(1, 'ANSI')]
|
||||
|
||||
class Ttype(object):
|
||||
"""
|
||||
Handles ttype negotiations. Called and initiated by the
|
||||
telnet protocol.
|
||||
"""
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
initialize ttype by storing protocol on ourselves and calling
|
||||
the client to see if it supporst ttype.
|
||||
|
||||
the ttype_step indicates how far in the data retrieval we've
|
||||
gotten.
|
||||
"""
|
||||
self.ttype_step = 0
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags['TTYPE'] = {"init_done": False}
|
||||
# is it a safe bet to assume ANSI is always supported?
|
||||
self.protocol.protocol_flags['TTYPE']['ANSI'] = True
|
||||
# setup protocol to handle ttype initialization and negotiation
|
||||
self.protocol.negotiationMap[TTYPE] = self.will_ttype
|
||||
# ask if client will ttype, connect callback if it does.
|
||||
self.protocol.do(TTYPE).addCallbacks(self.will_ttype, self.wont_ttype)
|
||||
|
||||
def wont_ttype(self, option):
|
||||
"""
|
||||
Callback if ttype is not supported by client.
|
||||
"""
|
||||
self.protocol.protocol_flags['TTYPE']["init_done"] = True
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def will_ttype(self, option):
|
||||
"""
|
||||
Handles negotiation of the ttype protocol once the
|
||||
client has confirmed that it will respond with the ttype
|
||||
protocol.
|
||||
|
||||
The negotiation proceeds in several steps, each returning a
|
||||
certain piece of information about the client. All data is
|
||||
stored on protocol.protocol_flags under the TTYPE key.
|
||||
"""
|
||||
options = self.protocol.protocol_flags.get('TTYPE')
|
||||
|
||||
if options and options.get('init_done') or self.ttype_step > 3:
|
||||
return
|
||||
|
||||
try:
|
||||
option = "".join(option).lstrip(IS)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
#print "incoming TTYPE option:", option
|
||||
|
||||
if self.ttype_step == 0:
|
||||
# just start the request chain
|
||||
self.protocol.requestNegotiation(TTYPE, SEND)
|
||||
|
||||
elif self.ttype_step == 1:
|
||||
# this is supposed to be the name of the client/terminal.
|
||||
# For clients not supporting the extended TTYPE
|
||||
# definition, subsequent calls will just repeat-return this.
|
||||
clientname = option.upper()
|
||||
# use name to identify support for xterm256. Many of these
|
||||
# only support after a certain version, but all support
|
||||
# it since at least 4 years. We assume recent client here for now.
|
||||
xterm256 = False
|
||||
if clientname.startswith("MUDLET"):
|
||||
# supports xterm256 stably since 1.1 (2010?)
|
||||
xterm256 = clientname.split("MUDLET",1)[1].strip() >= "1.1"
|
||||
else:
|
||||
xterm256 = (clientname.startswith("XTERM") or
|
||||
clientname.endswith("-256COLOR") or
|
||||
clientname in ("ATLANTIS", # > 0.9.9.0 (aug 2009)
|
||||
"CMUD", # > 3.04 (mar 2009)
|
||||
"KILDCLIENT", # > 2.2.0 (sep 2005)
|
||||
"MUDLET", # > beta 15 (sep 2009)
|
||||
"MUSHCLIENT", # > 4.02 (apr 2007)
|
||||
"PUTTY", # > 0.58 (apr 2005)
|
||||
"BEIP")) # > 2.00.206 (late 2009) (BeipMu)
|
||||
|
||||
# all clients supporting TTYPE at all seem to support ANSI
|
||||
self.protocol.protocol_flags['TTYPE']['ANSI'] = True
|
||||
self.protocol.protocol_flags['TTYPE']['256 COLORS'] = xterm256
|
||||
self.protocol.protocol_flags['TTYPE']['CLIENTNAME'] = clientname
|
||||
self.protocol.requestNegotiation(TTYPE, SEND)
|
||||
|
||||
elif self.ttype_step == 2:
|
||||
# this is a term capabilities flag
|
||||
term = option
|
||||
# identify xterm256 based on flag
|
||||
xterm256 = (term.endswith("-256color") # Apple Terminal, old Tintin
|
||||
or term.endswith("xterm") and # old Tintin, Putty
|
||||
not term.endswith("-color"))
|
||||
if xterm256:
|
||||
self.protocol.protocol_flags['TTYPE']['ANSI'] = True
|
||||
self.protocol.protocol_flags['TTYPE']['256 COLORS'] = xterm256
|
||||
self.protocol.protocol_flags['TTYPE']['TERM'] = term
|
||||
# request next information
|
||||
self.protocol.requestNegotiation(TTYPE, SEND)
|
||||
|
||||
elif self.ttype_step == 3:
|
||||
# the MTTS bitstring identifying term capabilities
|
||||
if option.startswith("MTTS"):
|
||||
option = option.split(" ")[1]
|
||||
if option.isdigit():
|
||||
# a number - determine the actual capabilities
|
||||
option = int(option)
|
||||
support = dict((capability, True) for bitval, capability in MTTS if option & bitval > 0)
|
||||
self.protocol.protocol_flags['TTYPE'].update(support)
|
||||
else:
|
||||
# some clients send erroneous MTTS as a string. Add directly.
|
||||
self.protocol.protocol_flags['TTYPE'][option.upper()] = True
|
||||
|
||||
self.protocol.protocol_flags['TTYPE']['init_done'] = True
|
||||
# print "TTYPE final:", self.protocol.protocol_flags['TTYPE']
|
||||
# we must sync ttype once it'd done
|
||||
self.protocol.handshake_done()
|
||||
self.ttype_step += 1
|
||||
252
lib/server/portal/webclient.py
Normal file
252
lib/server/portal/webclient.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"""
|
||||
Web client server resource.
|
||||
|
||||
The Evennia web client consists of two components running
|
||||
on twisted and django. They are both a part of the Evennia
|
||||
website url tree (so the testing website might be located
|
||||
on http://localhost:8000/, whereas the webclient can be
|
||||
found on http://localhost:8000/webclient.)
|
||||
|
||||
/webclient - this url is handled through django's template
|
||||
system and serves the html page for the client
|
||||
itself along with its javascript chat program.
|
||||
/webclientdata - this url is called by the ajax chat using
|
||||
POST requests (long-polling when necessary)
|
||||
The WebClient resource in this module will
|
||||
handle these requests and act as a gateway
|
||||
to sessions connected over the webclient.
|
||||
"""
|
||||
import time
|
||||
import json
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
from twisted.web import server, resource
|
||||
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.encoding import force_unicode
|
||||
from django.conf import settings
|
||||
from src.utils import utils, logger
|
||||
from src.utils.text2html import parse_html
|
||||
from src.server import session
|
||||
|
||||
SERVERNAME = settings.SERVERNAME
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
|
||||
# defining a simple json encoder for returning
|
||||
# django data to the client. Might need to
|
||||
# extend this if one wants to send more
|
||||
# complex database objects too.
|
||||
|
||||
class LazyEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Promise):
|
||||
return force_unicode(obj)
|
||||
return super(LazyEncoder, self).default(obj)
|
||||
|
||||
|
||||
def jsonify(obj):
|
||||
return utils.to_str(json.dumps(obj, ensure_ascii=False, cls=LazyEncoder))
|
||||
|
||||
|
||||
#
|
||||
# WebClient resource - this is called by the ajax client
|
||||
# using POST requests to /webclientdata.
|
||||
#
|
||||
|
||||
class WebClient(resource.Resource):
|
||||
"""
|
||||
An ajax/comet long-polling transport
|
||||
"""
|
||||
isLeaf = True
|
||||
allowedMethods = ('POST',)
|
||||
|
||||
def __init__(self):
|
||||
self.requests = {}
|
||||
self.databuffer = {}
|
||||
|
||||
#def getChild(self, path, request):
|
||||
# """
|
||||
# This is the place to put dynamic content.
|
||||
# """
|
||||
# return self
|
||||
|
||||
def _responseFailed(self, failure, suid, request):
|
||||
"callback if a request is lost/timed out"
|
||||
try:
|
||||
del self.requests[suid]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def lineSend(self, suid, string, data=None):
|
||||
"""
|
||||
This adds the data to the buffer and/or sends it to
|
||||
the client as soon as possible.
|
||||
"""
|
||||
request = self.requests.get(suid)
|
||||
if request:
|
||||
# we have a request waiting. Return immediately.
|
||||
request.write(jsonify({'msg': string, 'data': data}))
|
||||
request.finish()
|
||||
del self.requests[suid]
|
||||
else:
|
||||
# no waiting request. Store data in buffer
|
||||
dataentries = self.databuffer.get(suid, [])
|
||||
dataentries.append(jsonify({'msg': string, 'data': data}))
|
||||
self.databuffer[suid] = dataentries
|
||||
|
||||
def client_disconnect(self, suid):
|
||||
"""
|
||||
Disconnect session with given suid.
|
||||
"""
|
||||
if suid in self.requests:
|
||||
self.requests[suid].finish()
|
||||
del self.requests[suid]
|
||||
if suid in self.databuffer:
|
||||
del self.databuffer[suid]
|
||||
|
||||
def mode_init(self, request):
|
||||
"""
|
||||
This is called by render_POST when the client
|
||||
requests an init mode operation (at startup)
|
||||
"""
|
||||
#csess = request.getSession() # obs, this is a cookie, not
|
||||
# an evennia session!
|
||||
#csees.expireCallbacks.append(lambda : )
|
||||
suid = request.args.get('suid', ['0'])[0]
|
||||
|
||||
remote_addr = request.getClientIP()
|
||||
host_string = "%s (%s:%s)" % (SERVERNAME, request.getRequestHostname(), request.getHost().port)
|
||||
if suid == '0':
|
||||
# creating a unique id hash string
|
||||
suid = md5(str(time.time())).hexdigest()
|
||||
self.databuffer[suid] = []
|
||||
|
||||
sess = WebClientSession()
|
||||
sess.client = self
|
||||
sess.init_session("webclient", remote_addr, self.sessionhandler)
|
||||
sess.suid = suid
|
||||
sess.sessionhandler.connect(sess)
|
||||
return jsonify({'msg': host_string, 'suid': suid})
|
||||
|
||||
def mode_input(self, request):
|
||||
"""
|
||||
This is called by render_POST when the client
|
||||
is sending data to the server.
|
||||
"""
|
||||
suid = request.args.get('suid', ['0'])[0]
|
||||
if suid == '0':
|
||||
return ''
|
||||
sess = self.sessionhandler.session_from_suid(suid)
|
||||
if sess:
|
||||
sess = sess[0]
|
||||
text = request.args.get('msg', [''])[0]
|
||||
data = request.args.get('data', [None])[0]
|
||||
sess.sessionhandler.data_in(sess, text, data=data)
|
||||
return ''
|
||||
|
||||
def mode_receive(self, request):
|
||||
"""
|
||||
This is called by render_POST when the client is telling us
|
||||
that it is ready to receive data as soon as it is
|
||||
available. This is the basis of a long-polling (comet)
|
||||
mechanism: the server will wait to reply until data is
|
||||
available.
|
||||
"""
|
||||
suid = request.args.get('suid', ['0'])[0]
|
||||
if suid == '0':
|
||||
return ''
|
||||
|
||||
dataentries = self.databuffer.get(suid, [])
|
||||
if dataentries:
|
||||
return dataentries.pop(0)
|
||||
request.notifyFinish().addErrback(self._responseFailed, suid, request)
|
||||
if suid in self.requests:
|
||||
self.requests[suid].finish() # Clear any stale request.
|
||||
self.requests[suid] = request
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
def mode_close(self, request):
|
||||
"""
|
||||
This is called by render_POST when the client is signalling
|
||||
that it is about to be closed.
|
||||
"""
|
||||
suid = request.args.get('suid', ['0'])[0]
|
||||
if suid == '0':
|
||||
self.client_disconnect(suid)
|
||||
else:
|
||||
try:
|
||||
sess = self.sessionhandler.session_from_suid(suid)[0]
|
||||
sess.sessionhandler.disconnect(sess)
|
||||
except IndexError:
|
||||
self.client_disconnect(suid)
|
||||
pass
|
||||
return ''
|
||||
|
||||
def render_POST(self, request):
|
||||
"""
|
||||
This function is what Twisted calls with POST requests coming
|
||||
in from the ajax client. The requests should be tagged with
|
||||
different modes depending on what needs to be done, such as
|
||||
initializing or sending/receving data through the request. It
|
||||
uses a long-polling mechanism to avoid sending data unless
|
||||
there is actual data available.
|
||||
"""
|
||||
dmode = request.args.get('mode', [None])[0]
|
||||
if dmode == 'init':
|
||||
# startup. Setup the server.
|
||||
return self.mode_init(request)
|
||||
elif dmode == 'input':
|
||||
# input from the client to the server
|
||||
return self.mode_input(request)
|
||||
elif dmode == 'receive':
|
||||
# the client is waiting to receive data.
|
||||
return self.mode_receive(request)
|
||||
elif dmode == 'close':
|
||||
# the client is closing
|
||||
return self.mode_close(request)
|
||||
else:
|
||||
# this should not happen if client sends valid data.
|
||||
return ''
|
||||
|
||||
|
||||
#
|
||||
# A session type handling communication over the
|
||||
# web client interface.
|
||||
#
|
||||
|
||||
class WebClientSession(session.Session):
|
||||
"""
|
||||
This represents a session running in a webclient.
|
||||
"""
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Disconnect from server
|
||||
"""
|
||||
if reason:
|
||||
self.client.lineSend(self.suid, reason)
|
||||
self.client.client_disconnect(self.suid)
|
||||
|
||||
def data_out(self, text=None, **kwargs):
|
||||
"""
|
||||
Data Evennia -> Player access hook.
|
||||
|
||||
webclient flags checked are
|
||||
raw=True - no parsing at all (leave ansi-to-html markers unparsed)
|
||||
nomarkup=True - clean out all ansi/html markers and tokens
|
||||
|
||||
"""
|
||||
# string handling is similar to telnet
|
||||
try:
|
||||
text = utils.to_str(text if text else "", encoding=self.encoding)
|
||||
raw = kwargs.get("raw", False)
|
||||
nomarkup = kwargs.get("nomarkup", False)
|
||||
if raw:
|
||||
self.client.lineSend(self.suid, text)
|
||||
else:
|
||||
self.client.lineSend(self.suid,
|
||||
parse_html(text, strip_ansi=nomarkup))
|
||||
return
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
135
lib/server/portal/websocket_client.py
Normal file
135
lib/server/portal/websocket_client.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"""
|
||||
Websocket-webclient
|
||||
|
||||
This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
|
||||
by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is
|
||||
used together with src/web/media/javascript/evennia_websocket_webclient.js.
|
||||
|
||||
Thanks to Ricard Pillosu whose Evennia plugin inspired this module.
|
||||
|
||||
Communication over the websocket interface is done with normal text
|
||||
communication. A special case is OOB-style communication; to do this
|
||||
the client must send data on the following form:
|
||||
|
||||
OOB{"func1":[args], "func2":[args], ...}
|
||||
|
||||
where the dict is JSON encoded. The initial OOB-prefix
|
||||
is used to identify this type of communication, all other data
|
||||
is considered plain text (command input).
|
||||
|
||||
Example of call from a javascript client:
|
||||
|
||||
websocket = new WeSocket("ws://localhost:8021")
|
||||
var msg1 = "WebSocket Test"
|
||||
websocket.send(msg1)
|
||||
var msg2 = JSON.stringify({"testfunc":[[1,2,3], {"kwarg":"val"}]})
|
||||
websocket.send("OOB" + msg2)
|
||||
websocket.close()
|
||||
|
||||
"""
|
||||
import json
|
||||
from twisted.internet.protocol import Protocol
|
||||
from src.server.session import Session
|
||||
from src.utils.logger import log_trace
|
||||
from src.utils.utils import to_str, make_iter
|
||||
from src.utils.text2html import parse_html
|
||||
|
||||
|
||||
class WebSocketClient(Protocol, Session):
|
||||
"""
|
||||
Implements the server-side of the Websocket connection.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
"""
|
||||
This is called when the connection is first established.
|
||||
"""
|
||||
client_address = self.transport.client
|
||||
self.init_session("websocket", client_address, self.factory.sessionhandler)
|
||||
# watch for dead links
|
||||
self.transport.setTcpKeepAlive(1)
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
generic hook for the engine to call in order to
|
||||
disconnect this protocol.
|
||||
"""
|
||||
if reason:
|
||||
self.data_out(text=reason)
|
||||
self.connectionLost(reason)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
this is executed when the connection is lost for
|
||||
whatever reason. it can also be called directly, from
|
||||
the disconnect method
|
||||
"""
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.transport.close()
|
||||
|
||||
def dataReceived(self, string):
|
||||
"""
|
||||
Method called when data is coming in over
|
||||
the websocket connection.
|
||||
|
||||
Type of data is identified by a 3-character
|
||||
prefix.
|
||||
OOB - This is an Out-of-band instruction. If so,
|
||||
the remaining string should be a json-packed
|
||||
string on the form {oobfuncname: [args, ], ...}
|
||||
any other prefix (or lack of prefix) is considered
|
||||
plain text data, to be treated like a game
|
||||
input command.
|
||||
"""
|
||||
if string[:3] == "OOB":
|
||||
string = string[3:]
|
||||
try:
|
||||
oobdata = json.loads(string)
|
||||
for (key, args) in oobdata.items():
|
||||
#print "oob data in:", (key, args)
|
||||
self.data_in(text=None, oob=(key, make_iter(args)))
|
||||
except Exception:
|
||||
log_trace("Websocket malformed OOB request: %s" % string)
|
||||
else:
|
||||
# plain text input
|
||||
self.data_in(text=string)
|
||||
|
||||
def sendLine(self, line):
|
||||
"send data to client"
|
||||
return self.transport.write(line)
|
||||
|
||||
def data_in(self, text=None, **kwargs):
|
||||
"""
|
||||
Data Websocket -> Server
|
||||
"""
|
||||
self.sessionhandler.data_in(self, text=text, **kwargs)
|
||||
|
||||
def data_out(self, text=None, **kwargs):
|
||||
"""
|
||||
Data Evennia -> Player.
|
||||
generic hook method for engine to call in order to send data
|
||||
through the websocket connection.
|
||||
|
||||
valid webclient kwargs:
|
||||
oob=<string> - supply an Out-of-Band instruction.
|
||||
raw=True - no parsing at all (leave ansi-to-html markers unparsed)
|
||||
nomarkup=True - clean out all ansi/html markers and tokens
|
||||
"""
|
||||
try:
|
||||
text = to_str(text if text else "", encoding=self.encoding)
|
||||
except Exception, e:
|
||||
self.sendLine(str(e))
|
||||
if "oob" in kwargs:
|
||||
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
|
||||
#print "oob data_out:", "OOB" + json.dumps(oobstruct)
|
||||
self.sendLine("OOB" + json.dumps(oobstruct))
|
||||
raw = kwargs.get("raw", False)
|
||||
nomarkup = kwargs.get("nomarkup", False)
|
||||
if "prompt" in kwargs:
|
||||
self.sendLine("PROMPT" + parse_html(kwargs["prompt"], strip_ansi=nomarkup))
|
||||
if raw:
|
||||
self.sendLine(text)
|
||||
else:
|
||||
self.sendLine(parse_html(text, strip_ansi=nomarkup))
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue