Refactored src/server, splitting out into a portal subdirectory to make it clearer what goes on which "side".

This commit is contained in:
Griatch 2013-05-22 18:40:16 +02:00
parent 76fa0059ea
commit 812bdb0f73
13 changed files with 179 additions and 175 deletions

View file

63
src/server/portal/mccp.py Normal file
View file

@ -0,0 +1,63 @@
"""
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):
"""
If client doesn't support mccp, don't do anything.
"""
if hasattr(self.protocol, 'zlib'):
del self.protocol.zlib
self.protocol.protocol_flags['MCCP'] = False
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)

329
src/server/portal/msdp.py Normal file
View file

@ -0,0 +1,329 @@
"""
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.
!TODO - this is just a partial implementation and not used by telnet yet.
"""
import re
from django.conf import settings
from src.utils.utils import make_iter, mod_import
from src.utils import logger
# 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)
# pre-compiled regexes
regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY_OPEN, MSDP_ARRAY_CLOSE)) # return 2-tuple
regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE_OPEN, MSDP_TABLE_CLOSE)) # return 2-tuple (may be nested)
regex_varval = re.compile(r"%s(.*?)%s(.*?)" % (MSDP_VAR, MSDP_VAL)) # return 2-tuple
# MSDP default definition commands supported by Evennia (can be supplemented with custom commands as well)
MSDP_COMMANDS = ("LIST", "REPORT", "RESET", "SEND", "UNREPORT")
# fallbacks if no custom OOB module is available
MSDP_COMMANDS_CUSTOM = {}
# MSDP_REPORTABLE is a standard suggestions for making it easy to create generic guis.
# this maps MSDP command names to Evennia commands found in OOB_FUNC_MODULE. It
# is up to these commands to return data on proper form. This is overloaded if
# OOB_REPORTABLE is defined in the custom OOB module below.
MSDP_REPORTABLE = {
# General
"CHARACTER_NAME": "get_character_name",
"SERVER_ID": "get_server_id",
"SERVER_TIME": "get_server_time",
# Character
"AFFECTS": "char_affects",
"ALIGNMENT": "char_alignment",
"EXPERIENCE": "char_experience",
"EXPERIENCE_MAX": "char_experience_max",
"EXPERIENCE_TNL": "char_experience_tnl",
"HEALTH": "char_health",
"HEALTH_MAX": "char_health_max",
"LEVEL": "char_level",
"RACE": "char_race",
"CLASS": "char_class",
"MANA": "char_mana",
"MANA_MAX": "char_mana_max",
"WIMPY": "char_wimpy",
"PRACTICE": "char_practice",
"MONEY": "char_money",
"MOVEMENT": "char_movement",
"MOVEMENT_MAX": "char_movement_max",
"HITROLL": "char_hitroll",
"DAMROLL": "char_damroll",
"AC": "char_ac",
"STR": "char_str",
"INT": "char_int",
"WIS": "char_wis",
"DEX": "char_dex",
"CON": "char_con",
# Combat
"OPPONENT_HEALTH": "opponent_health",
"OPPONENT_HEALTH_MAX":"opponent_health_max",
"OPPONENT_LEVEL": "opponent_level",
"OPPONENT_NAME": "opponent_name",
# World
"AREA_NAME": "area_name",
"ROOM_EXITS": "area_room_exits",
"ROOM_NAME": "room_name",
"ROOM_VNUM": "room_dbref",
"WORLD_TIME": "world_time",
# Configurable variables
"CLIENT_ID": "client_id",
"CLIENT_VERSION": "client_version",
"PLUGIN_ID": "plugin_id",
"ANSI_COLORS": "ansi_colours",
"XTERM_256_COLORS": "xterm_256_colors",
"UTF_8": "utf_8",
"SOUND": "sound",
"MXP": "mxp",
# GUI variables
"BUTTON_1": "button1",
"BUTTON_2": "button2",
"BUTTON_3": "button3",
"BUTTON_4": "button4",
"BUTTON_5": "button5",
"GAUGE_1": "gauge1",
"GAUGE_2": "gauge2",
"GAUGE_3": "gauge3",
"GAUGE_4": "gauge4",
"GAUGE_5": "gauge5"}
MSDP_SENDABLE = MSDP_REPORTABLE
# try to load custom OOB module
OOB_MODULE = mod_import(settings.OOB_FUNC_MODULE)
if OOB_MODULE:
# loading customizations from OOB_FUNC_MODULE if available
try: MSDP_REPORTABLE = OOB_MODULE.OOB_REPORTABLE # replaces the default MSDP definitions
except AttributeError: pass
try: MSDP_SENDABLE = OOB_MODULE.OOB_SENDABLE
except AttributeError: MSDP_SENDABLE = MSDP_REPORTABLE
try: MSDP_COMMANDS_CUSTOM = OOB_MODULE.OOB_COMMANDS
except: pass
# 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.parse_msdp
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
self.msdp_reported = {}
def no_msdp(self, option):
"No msdp supported or wanted"
pass
def do_msdp(self, option):
"""
Called when client confirms that it can do MSDP.
"""
self.protocol.protocol_flags['MSDP'] = True
def func_to_msdp(self, cmdname, data):
"""
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 - this supports nested tables and even arrays nested
inside tables, as opposed to the receive method. Arrays
cannot hold tables by definition (the table must be named
with MSDP_VAR, and an array can only contain MSDP_VALs).
"""
def make_table(name, datadict, string):
"build a table that may be nested with other tables or arrays."
string += MSDP_VAR + name + MSDP_VAL + MSDP_TABLE_OPEN
for key, val in datadict.items():
if type(val) == type({}):
string += make_table(key, val, string)
elif hasattr(val, '__iter__'):
string += make_array(key, val, string)
else:
string += MSDP_VAR + key + MSDP_VAL + val
string += MSDP_TABLE_CLOSE
return string
def make_array(name, string, datalist):
"build a simple array. Arrays may not nest tables by definition."
string += MSDP_VAR + name + MSDP_ARRAY_OPEN
for val in datalist:
string += MSDP_VAL + val
string += MSDP_ARRAY_CLOSE
return string
if type(data) == type({}):
msdp_string = make_table(cmdname, data, "")
elif hasattr(data, '__iter__'):
msdp_string = make_array(cmdname, data, "")
else:
msdp_string = MSDP_VAR + cmdname + MSDP_VAL + data
return msdp_string
def msdp_to_func(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 = {}
for table in regex_table.findall(data):
tables[table[0].upper()] = dict(regex_varval(table[1]))
for array in regex_array.findall(data):
arrays[array[0].upper()] = dict(regex_varval(array[1]))
variables = dict((key.upper(), val) for key, val in regex_varval(regex_array.sub("", regex_table.sub("", data))))
ret = ""
# default MSDP functions
if "LIST" in variables:
ret += self.func_to_msdp("LIST", self.msdp_cmd_list(variables["LIST"]))
del variables["LIST"]
if "REPORT" in variables:
ret += self.func_to_msdp("REPORT", self.msdp_cmd_report(*(variables["REPORT"],)))
del variables["REPORT"]
if "REPORT" in arrays:
ret += self.func_to_msdp("REPORT", self.msdp_cmd_report(*arrays["REPORT"]))
del arrays["REPORT"]
if "RESET" in variables:
ret += self.func_to_msdp("RESET", self.msdp_cmd_reset(*(variables["RESET"],)))
del variables["RESET"]
if "RESET" in arrays:
ret += self.func_to_msdp("RESET", self.msdp_cmd_reset(*arrays["RESET"]))
del arrays["RESET"]
if "SEND" in variables:
ret += self.func_to_msdp("SEND", self.msdp_cmd_send(*(variables["SEND"],)))
del variables["SEND"]
if "SEND" in arrays:
ret += self.func_to_msdp("SEND",self.msdp_cmd_send(*arrays["SEND"]))
del arrays["SEND"]
# if there are anything left we look for a custom function
for varname, var in variables.items():
# a simple function + argument
ooc_func = MSDP_COMMANDS_CUSTOM.get(varname.upper())
if ooc_func:
ret += self.func_to_msdp(varname, ooc_func(var))
for arrayname, array in arrays.items():
# we assume the array are multiple arguments to the function
ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper())
if ooc_func:
ret += self.func_to_msdp(arrayname, ooc_func(*array))
for tablename, table in tables.items():
# we assume tables are keyword arguments to the function
ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper())
if ooc_func:
ret += self.func_to_msdp(tablename, ooc_func(**table))
return ret
# MSDP Commands
# Some given MSDP (varname, value) pairs can also be treated as command + argument.
# Generic msdp command map. The argument will be sent to the given command.
# See http://tintin.sourceforge.net/msdp/ for definitions of each command.
# These are client->server commands.
def msdp_cmd_list(self, arg):
"""
The List command allows for retrieving various info about the server/client
"""
if arg == 'COMMANDS':
return self.func_to_msdp(arg, MSDP_COMMANDS)
elif arg == 'LISTS':
return self.func_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES",
"REPORTED_VARIABLES", "SENDABLE_VARIABLES"))
elif arg == 'CONFIGURABLE_VARIABLES':
return self.func_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID"))
elif arg == 'REPORTABLE_VARIABLES':
return self.func_to_msdp(arg, MSDP_REPORTABLE.keys())
elif arg == 'REPORTED_VARIABLES':
# the dynamically set items to report
return self.func_to_msdp(arg, self.msdp_reported.keys())
elif arg == 'SENDABLE_VARIABLES':
return self.func_to_msdp(arg, MSDP_SENDABLE.keys())
else:
return self.func_to_msdp("LIST", arg)
# default msdp commands
def msdp_cmd_report(self, *arg):
"""
The report command instructs the server to start reporting a
reportable variable to the client.
"""
try:
MSDP_REPORTABLE[arg](report=True)
except Exception:
logger.log_trace()
def msdp_cmd_unreport(self, arg):
"""
Unreport a previously reported variable
"""
try:
MSDP_REPORTABLE[arg](report=False)
except Exception:
self.logger.log_trace()
def msdp_cmd_reset(self, arg):
"""
The reset command resets a variable to its initial state.
"""
try:
MSDP_REPORTABLE[arg](reset=True)
except Exception:
logger.log_trace()
def msdp_cmd_send(self, arg):
"""
Request the server to send a particular variable
to the client.
arg - this is a list of variables the client wants.
"""
ret = []
for var in make_iter(arg):
try:
ret.append(MSDP_REPORTABLE[arg](send=True))
except Exception:
logger.log_trace()
return ret

184
src/server/portal/mssp.py Normal file
View file

@ -0,0 +1,184 @@
"""
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.
Most of these settings are de
"""
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.
"""
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)

294
src/server/portal/portal.py Normal file
View file

@ -0,0 +1,294 @@
"""
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 twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.web import server, static
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
TELNET_INTERFACES = settings.TELNET_INTERFACES
SSL_INTERFACES = settings.SSL_INTERFACES
SSH_INTERFACES = settings.SSH_INTERFACES
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
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
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
AMP_ENABLED = AMP_HOST and AMP_PORT
#------------------------------------------------------------
# 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 == 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
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:
if ":" in interface:
print " iPv6 interfaces not yet supported"
continue
ifacestr = ""
if interface != '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:
if ":" in interface:
print " iPv6 interfaces not yet supported"
continue
ifacestr = ""
if interface != '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:
if ":" in interface:
print " iPv6 interfaces not yet supported"
continue
ifacestr = ""
if interface != '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 django-compatible webserver.
from twisted.python import threadpool
from src.server.webserver import DjangoWebRoot, WSGIWebServer
# start a thread pool and define the root url (/) as a wsgi resource
# recognized by Django
threads = threadpool.ThreadPool()
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
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 = "/client"
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
for interface in WEBSERVER_INTERFACES:
if ":" in interface:
print " iPv6 interfaces not yet supported"
continue
ifacestr = ""
if interface != '0.0.0.0' or len(WEBSERVER_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in WEBSERVER_PORTS:
pstring = "%s:%s" % (ifacestr, port)
# create the webserver
webserver = WSGIWebServer(threads, port, web_site, interface=interface)
webserver.setName('EvenniaWebServer%s' % pstring)
PORTAL.services.addService(webserver)
print " webserver%s%s: %s" % (webclientstr, ifacestr, port)
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()

View file

@ -0,0 +1,167 @@
"""
Sessionhandler for portal sessions
"""
import time
from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN
#------------------------------------------------------------
# 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
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation=PCONN,
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_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, string="", data=""):
"""
Called by portal sessions for relaying data coming
in from the protocol to the server. data is
serialized before passed on.
"""
#print "portal_data_in:", string
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
msg=string,
data=data)
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, string="", data=""):
"""
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(string, data=data)
def oob_data_in(self, session, data):
"""
OOB (Out-of-band) data Portal -> Server
"""
print "portal_oob_data_in:", data
self.portal.amp_protocol.call_remote_OOBPortal2Server(session.sessid,
data=data)
def oob_data_out(self, sessid, data):
"""
OOB (Out-of-band) data Server -> Portal
"""
print "portal_oob_data_out:", data
session = self.sessions.get(sessid, None)
if session:
session.oob_data_out(data)
PORTAL_SESSIONS = PortalSessionHandler()

348
src/server/portal/ssh.py Normal file
View file

@ -0,0 +1,348 @@
"""
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, logger
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]
self.cfactory = starttuple[1] # obs may not be called self.factory, it gets overwritten!
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'):
self.terminal.write(line) #this is the telnet-specific method for sending
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, string, data=None):
"""
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
"""
try:
string = utils.to_str(string, encoding=self.encoding)
except Exception, e:
self.lineSend(str(e))
return
nomarkup = False
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", False)
# check if we want to remove all markup
nomarkup = data.get("nomarkup", False)
if raw:
self.lineSend(string)
else:
self.lineSend(ansi.parse_ansi(string.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

77
src/server/portal/ssl.py Normal file
View file

@ -0,0 +1,77 @@
"""
This is a simple context factory for auto-creating
SSL keys and certificates.
"""
import os, 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:
err = subprocess.call(exestring)#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
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)

168
src/server/portal/telnet.py Normal file
View file

@ -0,0 +1,168 @@
"""
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
from src.server.session import Session
from src.server.portal import ttype, mssp
from src.server.portal.mccp import Mccp, mccp_compress, MCCP
from src.utils import utils, ansi, logger
_RE_N = re.compile(r"\{n$")
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
client_address = self.transport.client
self.init_session("telnet", client_address, self.factory.sessionhandler)
# negotiate mccp (data compression)
self.mccp = Mccp(self)
# negotiate ttype (client info)
self.ttype = ttype.Ttype(self)
# negotiate mssp (crawler communication)
self.mssp = mssp.Mssp(self)
# add this new connection to sessionhandler so
# the Server becomes aware of it.
self.sessionhandler.connect(self)
def enableRemote(self, option):
"""
This sets up the options we allow for this protocol.
"""
return (option == LINEMODE or
option == ttype.TTYPE or
option == MCCP or
option == mssp.MSSP)
def enableLocal(self, option):
"""
Allow certain options on this protocol
"""
return option == MCCP
def disableLocal(self, option):
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.
"""
#print "dataRcv (%s):" % data,
#try:
# for b in data:
# print ord(b),
# print ""
#except Exception, e:
# print str(e) + ":", str(data)
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:
logger.log_trace()
# if we get to this point the command must end with a linebreak.
# We make sure to add it, to fix some clients messing this up.
data = data.rstrip("\r\n") + "\n"
#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.sessionhandler.data_in(self, 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_out(self, string, data=None):
"""
generic hook method for engine to call in order to send data
through the telnet connection.
Data Evennia -> Player.
data argument may contain a dict with output flags.
"""
try:
string = utils.to_str(string, encoding=self.encoding)
except Exception, e:
self.sendLine(str(e))
return
ttype = self.protocol_flags.get('TTYPE', {})
nomarkup = not (ttype or ttype.get('256 COLORS') or ttype.get('ANSI') or not ttype.get("init_done"))
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", False)
# check if we want to remove all markup (TTYPE override)
nomarkup = data.get("nomarkup", False)
if raw:
self.sendLine(string)
else:
# we need to make sure to kill the color at the end in order to match the webclient output.
self.sendLine(ansi.parse_ansi(_RE_N.sub("", string) + "{n", strip_ansi=nomarkup, xterm256=ttype.get('256 COLORS')))

115
src/server/portal/ttype.py Normal file
View file

@ -0,0 +1,115 @@
"""
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')]
# some clients sends erroneous strings instead
# of capability numbers. We try to convert back.
MTTS_invert = {"PROXY":128,
"SCREEN COLOR PALETTE":64,
"OSC COLOR PALETTE": 32,
"MOUSE TRACKING": 16,
"256 COLORS": 8,
"UTF-8": 4,
"VT100": 2,
"ANSI": 1}
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}
# setup protocol to handle ttype initialization and negotiation
self.protocol.negotiationMap[TTYPE] = self.do_ttype
# ask if client will ttype, connect callback if it does.
self.protocol.will(TTYPE).addCallbacks(self.do_ttype, self.no_ttype)
def no_ttype(self, option):
"""
Callback if ttype is not supported by client.
"""
self.protocol.protocol_flags['TTYPE'] = {"init_done":True}
def do_ttype(self, option):
"""
Handles negotiation of the ttype protocol once the
client has confirmed that it supports 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.
"""
if self.protocol.protocol_flags['TTYPE']['init_done']:
return
self.ttype_step += 1
if self.ttype_step == 1:
# set up info storage and initialize subnegotiation
self.protocol.requestNegotiation(TTYPE, SEND)
else:
# receive data
option = "".join(option).lstrip(IS)
if self.ttype_step == 2:
self.protocol.protocol_flags['TTYPE']['CLIENTNAME'] = option
self.protocol.requestNegotiation(TTYPE, SEND)
elif self.ttype_step == 3:
self.protocol.protocol_flags['TTYPE']['TERM'] = option
self.protocol.requestNegotiation(TTYPE, SEND)
elif self.ttype_step == 4:
try:
option = int(option.strip('MTTS '))
except ValueError:
# it seems some clients don't send MTTS according to protocol
# specification, but instead just sends the data as plain
# strings. We try to convert back.
option = MTTS_invert.get(option.strip('MTTS ').upper())
if not option:
# no conversion possible. Give up.
self.protocol.protocol_flags['TTYPE']['init_done'] = True
return
self.protocol.protocol_flags['TTYPE']['MTTS'] = option
for codenum, standard in MTTS:
if option == 0:
break
status = option % codenum < option
self.protocol.protocol_flags['TTYPE'][standard] = status
if status:
option = option % codenum
self.protocol.protocol_flags['TTYPE']['init_done'] = True
#print "ttype results:", self.protocol.protocol_flags['TTYPE']

View file

@ -0,0 +1,256 @@
"""
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
from hashlib import md5
from twisted.web import server, resource
from twisted.internet import defer, reactor
from django.utils import simplejson
from django.utils.functional import Promise
from django.utils.encoding import force_unicode
from django.conf import settings
from src.utils import utils, logger, ansi
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(simplejson.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(simplejson.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 self.requests.has_key(suid):
self.requests[suid].finish()
del self.requests[suid]
if self.databuffer.has_key(suid):
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]
string = request.args.get('msg', [''])[0]
data = request.args.get('data', [None])[0]
sess.sessionhandler.data_in(sess, string, 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 self.requests.has_key(suid):
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, string='', data=None):
"""
Data Evennia -> Player access hook.
data argument may be used depending on
the client-server implementation.
"""
if data:
# treat data?
pass
# string handling is similar to telnet
try:
string = utils.to_str(string, encoding=self.encoding)
nomarkup = False
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", False)
# check if we want to remove all markup
nomarkup = data.get("nomarkup", False)
if raw:
self.client.lineSend(self.suid, string)
else:
self.client.lineSend(self.suid, parse_html(string, strip_ansi=nomarkup))
return
except Exception, e:
logger.log_trace()