Refactored src/server, splitting out into a portal subdirectory to make it clearer what goes on which "side".
This commit is contained in:
parent
76fa0059ea
commit
812bdb0f73
13 changed files with 179 additions and 175 deletions
0
src/server/portal/__init__.py
Normal file
0
src/server/portal/__init__.py
Normal file
63
src/server/portal/mccp.py
Normal file
63
src/server/portal/mccp.py
Normal 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
329
src/server/portal/msdp.py
Normal 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
184
src/server/portal/mssp.py
Normal 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
294
src/server/portal/portal.py
Normal 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()
|
||||
167
src/server/portal/portalsessionhandler.py
Normal file
167
src/server/portal/portalsessionhandler.py
Normal 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
348
src/server/portal/ssh.py
Normal 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
77
src/server/portal/ssl.py
Normal 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
168
src/server/portal/telnet.py
Normal 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
115
src/server/portal/ttype.py
Normal 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']
|
||||
256
src/server/portal/webclient.py
Normal file
256
src/server/portal/webclient.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue