Reshuffling the Evennia package into the new template paradigm.

This commit is contained in:
Griatch 2015-01-06 14:53:45 +01:00
parent 2846e64833
commit 2b3a32e447
371 changed files with 17250 additions and 304 deletions

10
lib/server/__init__.py Normal file
View file

@ -0,0 +1,10 @@
"""
Makes it easier to import by grouping all relevant things already at this level.
You can henceforth import most things directly from src.server
Also, the initiated object manager is available as src.server.manager.
"""
from src.server.models import *
manager = ServerConfig.objects

19
lib/server/admin.py Normal file
View file

@ -0,0 +1,19 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from django.contrib import admin
from src.server.models import ServerConfig
class ServerConfigAdmin(admin.ModelAdmin):
"Custom admin for server configs"
list_display = ('db_key', 'db_value')
list_display_links = ('db_key',)
ordering = ['db_key', 'db_value']
search_fields = ['db_key']
save_as = True
save_on_top = True
list_select_related = True
admin.site.register(ServerConfig, ServerConfigAdmin)

535
lib/server/amp.py Normal file
View file

@ -0,0 +1,535 @@
"""
Contains the protocols, commands, and client factory needed for the Server
and Portal to communicate with each other, letting Portal work as a proxy.
Both sides use this same protocol.
The separation works like this:
Portal - (AMP client) handles protocols. It contains a list of connected
sessions in a dictionary for identifying the respective player
connected. If it looses the AMP connection it will automatically
try to reconnect.
Server - (AMP server) Handles all mud operations. The server holds its own list
of sessions tied to player objects. This is synced against the portal
at startup and when a session connects/disconnects
"""
# imports needed on both server and portal side
import os
from collections import defaultdict
try:
import cPickle as pickle
except ImportError:
import pickle
from twisted.protocols import amp
from twisted.internet import protocol
from twisted.internet.defer import Deferred
from src.utils.utils import to_str, variable_from_module
# communication bits
PCONN = chr(1) # portal session connect
PDISCONN = chr(2) # portal session disconnect
PSYNC = chr(3) # portal session sync
SLOGIN = chr(4) # server session login
SDISCONN = chr(5) # server session disconnect
SDISCONNALL = chr(6) # server session disconnect all
SSHUTD = chr(7) # server shutdown
SSYNC = chr(8) # server session sync
SCONN = chr(9) # server creating new connectiong (for irc/imc2 bots etc)
PCONNSYNC = chr(10) # portal post-syncing a session
MAXLEN = 65535 # max allowed data length in AMP protocol
_MSGBUFFER = defaultdict(list)
def get_restart_mode(restart_file):
"""
Parse the server/portal restart status
"""
if os.path.exists(restart_file):
flag = open(restart_file, 'r').read()
return flag == "True"
return False
class AmpServerFactory(protocol.ServerFactory):
"""
This factory creates the Server as a new AMPProtocol instance for accepting
connections from the Portal.
"""
def __init__(self, server):
"""
server: The Evennia server service instance
protocol: The protocol the factory creates instances of.
"""
self.server = server
self.protocol = AMPProtocol
def buildProtocol(self, addr):
"""
Start a new connection, and store it on the service object
"""
#print "Evennia Server connected to Portal at %s." % addr
self.server.amp_protocol = AMPProtocol()
self.server.amp_protocol.factory = self
return self.server.amp_protocol
class AmpClientFactory(protocol.ReconnectingClientFactory):
"""
This factory creates an instance of the Portal, an AMPProtocol
instances to use to connect
"""
# Initial reconnect delay in seconds.
initialDelay = 1
factor = 1.5
maxDelay = 1
def __init__(self, portal):
self.portal = portal
self.protocol = AMPProtocol
def startedConnecting(self, connector):
"""
Called when starting to try to connect to the MUD server.
"""
pass
#print 'AMP started to connect:', connector
def buildProtocol(self, addr):
"""
Creates an AMPProtocol instance when connecting to the server.
"""
#print "Portal connected to Evennia server at %s." % addr
self.resetDelay()
self.portal.amp_protocol = AMPProtocol()
self.portal.amp_protocol.factory = self
return self.portal.amp_protocol
def clientConnectionLost(self, connector, reason):
"""
Called when the AMP connection to the MUD server is lost.
"""
if hasattr(self, "server_restart_mode"):
self.maxDelay = 1
else:
# Don't translate this; avoid loading django on portal side.
self.maxDelay = 10
self.portal.sessions.announce_all(" ... Portal lost connection to Server.")
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
def clientConnectionFailed(self, connector, reason):
"""
Called when an AMP connection attempt to the MUD server fails.
"""
if hasattr(self, "server_restart_mode"):
self.maxDelay = 1
else:
self.maxDelay = 10
self.portal.sessions.announce_all(" ...")
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
# AMP Communication Command types
class MsgPortal2Server(amp.Command):
"""
Message portal -> server
"""
key = "MsgPortal2Server"
arguments = [('sessid', amp.Integer()),
('ipart', amp.Integer()),
('nparts', amp.Integer()),
('msg', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class MsgServer2Portal(amp.Command):
"""
Message server -> portal
"""
key = "MsgServer2Portal"
arguments = [('sessid', amp.Integer()),
('ipart', amp.Integer()),
('nparts', amp.Integer()),
('msg', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class ServerAdmin(amp.Command):
"""
Portal -> Server
Sent when the portal needs to perform admin
operations on the server, such as when a new
session connects or resyncs
"""
key = "ServerAdmin"
arguments = [('sessid', amp.Integer()),
('ipart', amp.Integer()),
('nparts', amp.Integer()),
('operation', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class PortalAdmin(amp.Command):
"""
Server -> Portal
Sent when the server needs to perform admin
operations on the portal.
"""
key = "PortalAdmin"
arguments = [('sessid', amp.Integer()),
('ipart', amp.Integer()),
('nparts', amp.Integer()),
('operation', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class FunctionCall(amp.Command):
"""
Bidirectional
Sent when either process needs to call an
arbitrary function in the other.
"""
key = "FunctionCall"
arguments = [('module', amp.String()),
('function', amp.String()),
('args', amp.String()),
('kwargs', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = [('result', amp.String())]
# Helper functions
dumps = lambda data: to_str(pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
loads = lambda data: pickle.loads(to_str(data))
# multipart message store
#------------------------------------------------------------
# Core AMP protocol for communication Server <-> Portal
#------------------------------------------------------------
class AMPProtocol(amp.AMP):
"""
This is the protocol that the MUD server and the proxy server
communicate to each other with. AMP is a bi-directional protocol, so
both the proxy and the MUD use the same commands and protocol.
AMP specifies responder methods here and connect them to amp.Command
subclasses that specify the datatypes of the input/output of these methods.
"""
# helper methods
def connectionMade(self):
"""
This is called when a connection is established
between server and portal. AMP calls it on both sides,
so we need to make sure to only trigger resync from the
portal side.
"""
if hasattr(self.factory, "portal"):
# only the portal has the 'portal' property, so we know we are
# on the portal side and can initialize the connection.
sessdata = self.factory.portal.sessions.get_all_sync_data()
self.call_remote_ServerAdmin(0,
PSYNC,
data=sessdata)
self.factory.portal.sessions.at_server_connection()
if hasattr(self.factory, "server_restart_mode"):
del self.factory.server_restart_mode
# Error handling
def errback(self, e, info):
"error handler, to avoid dropping connections on server tracebacks."
f = e.trap(Exception)
print "AMP Error for %(info)s: %(e)s" % {'info': info,
'e': e.getErrorMessage()}
def safe_send(self, command, sessid, **kwargs):
"""
This helper method splits the sending of a message into
multiple parts with a maxlength of MAXLEN. This is to avoid
repetition in two sending commands. when calling this the
maximum length has already been exceeded. The max-length will
be checked for all kwargs and these will be used as argument
to the command. The command type must have keywords ipart and
nparts to track the parts and put them back together on the
other side.
Returns a deferred or a list of such
"""
to_send = [(key, [string[i:i+MAXLEN] for i in range(0, len(string), MAXLEN)])
for key, string in kwargs.items()]
nparts_max = max(len(part[1]) for part in to_send)
if nparts_max == 1:
# first try to send directly
return self.callRemote(command,
sessid=sessid,
ipart=0,
nparts=1,
**kwargs).addErrback(self.errback, command.key)
else:
# one or more parts were too long for MAXLEN.
#print "TooLong triggered!"
deferreds = []
for ipart in range(nparts_max):
part_kwargs = {}
for key, str_part in to_send:
try:
part_kwargs[key] = str_part[ipart]
except IndexError:
# means this kwarg needed fewer splits
part_kwargs[key] = ""
# send this part
#print "amp safe sending:", ipart, nparts_max, str_part
deferreds.append(self.callRemote(
command,
sessid=sessid,
ipart=ipart,
nparts=nparts_max,
**part_kwargs).addErrback(self.errback, command.key))
return deferreds
def safe_recv(self, command, sessid, ipart, nparts, **kwargs):
"""
Safely decode potentially split data coming over the wire. No
decoding or parsing is done here, only merging of data split
with safe_send().
If the data stream is not yet complete, this method will return
None, otherwise it will return a dictionary of the (possibly
merged) properties.
"""
global _MSGBUFFER
if nparts == 1:
# the most common case
return kwargs
else:
# part of a multi-part send
hashid = "%s_%s" % (command.key, sessid)
#print "amp safe receive:", ipart, nparts-1, kwargs
if ipart < nparts-1:
# not yet complete
_MSGBUFFER[hashid].append(kwargs)
return
else:
# all parts in place, put them back together
buf = _MSGBUFFER.pop(hashid) + [kwargs]
recv_kwargs = dict((key, "".join(kw[key] for kw in buf)) for key in kwargs)
return recv_kwargs
# Message definition + helper methods to call/create each message type
# Portal -> Server Msg
def amp_msg_portal2server(self, sessid, ipart, nparts, msg, data):
"""
Relays message to server. This method is executed on the Server.
Since AMP has a limit of 65355 bytes per message, it's possible the
data comes in multiple chunks; if so (nparts>1) we buffer the data
and wait for the remaining parts to arrive before continuing.
"""
#print "msg portal -> server (server side):", sessid, msg, data
ret = self.safe_recv(MsgPortal2Server, sessid, ipart, nparts,
text=msg, data=data)
if ret is not None:
self.factory.server.sessions.data_in(sessid,
text=ret["text"],
**loads(ret["data"]))
return {}
MsgPortal2Server.responder(amp_msg_portal2server)
def call_remote_MsgPortal2Server(self, sessid, msg, data=""):
"""
Access method called by the Portal and executed on the Portal.
"""
#print "msg portal->server (portal side):", sessid, msg, data
return self.safe_send(MsgPortal2Server, sessid,
msg=msg if msg is not None else "",
data=dumps(data))
# Server -> Portal message
def amp_msg_server2portal(self, sessid, ipart, nparts, msg, data):
"""
Relays message to Portal. This method is executed on the Portal.
"""
#print "msg server->portal (portal side):", sessid, msg
ret = self.safe_recv(MsgServer2Portal, sessid,
ipart, nparts, text=msg, data=data)
if ret is not None:
self.factory.portal.sessions.data_out(sessid,
text=ret["text"],
**loads(ret["data"]))
return {}
MsgServer2Portal.responder(amp_msg_server2portal)
def call_remote_MsgServer2Portal(self, sessid, msg, data=""):
"""
Access method called by the Server and executed on the Server.
"""
#print "msg server->portal (server side):", sessid, msg, data
return self.safe_send(MsgServer2Portal, sessid,
msg=msg if msg is not None else "",
data=dumps(data))
# Server administration from the Portal side
def amp_server_admin(self, sessid, ipart, nparts, operation, data):
"""
This allows the portal to perform admin
operations on the server. This is executed on the Server.
"""
ret = self.safe_recv(ServerAdmin, sessid, ipart, nparts,
operation=operation, data=data)
if ret is not None:
data = loads(ret["data"])
operation = ret["operation"]
server_sessionhandler = self.factory.server.sessions
#print "serveradmin (server side):", sessid, ord(operation), data
if operation == PCONN: # portal_session_connect
# create a new session and sync it
server_sessionhandler.portal_connect(data)
elif operation == PCONNSYNC: #portal_session_sync
server_sessionhandler.portal_session_sync(data)
elif operation == PDISCONN: # portal_session_disconnect
# session closed from portal side
self.factory.server.sessions.portal_disconnect(sessid)
elif operation == PSYNC: # portal_session_sync
# force a resync of sessions when portal reconnects to
# server (e.g. after a server reboot) the data kwarg
# contains a dict {sessid: {arg1:val1,...}}
# representing the attributes to sync for each
# session.
server_sessionhandler.portal_sessions_sync(data)
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
return {}
ServerAdmin.responder(amp_server_admin)
def call_remote_ServerAdmin(self, sessid, operation="", data=""):
"""
Access method called by the Portal and Executed on the Portal.
"""
#print "serveradmin (portal side):", sessid, ord(operation), data
data = dumps(data)
return self.safe_send(ServerAdmin, sessid, operation=operation, data=data)
# Portal administraton from the Server side
def amp_portal_admin(self, sessid, ipart, nparts, operation, data):
"""
This allows the server to perform admin
operations on the portal. This is executed on the Portal.
"""
#print "portaladmin (portal side):", sessid, ord(operation), data
ret = self.safe_recv(PortalAdmin, sessid, ipart, nparts,
operation=operation, data=data)
if ret is not None:
data = loads(data)
portal_sessionhandler = self.factory.portal.sessions
if operation == SLOGIN: # server_session_login
# a session has authenticated; sync it.
portal_sessionhandler.server_logged_in(sessid, data)
elif operation == SDISCONN: # server_session_disconnect
# the server is ordering to disconnect the session
portal_sessionhandler.server_disconnect(sessid, reason=data)
elif operation == SDISCONNALL: # server_session_disconnect_all
# server orders all sessions to disconnect
portal_sessionhandler.server_disconnect_all(reason=data)
elif operation == SSHUTD: # server_shutdown
# the server orders the portal to shut down
self.factory.portal.shutdown(restart=False)
elif operation == SSYNC: # server_session_sync
# server wants to save session data to the portal,
# maybe because it's about to shut down.
portal_sessionhandler.server_session_sync(data)
# set a flag in case we are about to shut down soon
self.factory.server_restart_mode = True
elif operation == SCONN: # server_force_connection (for irc/imc2 etc)
portal_sessionhandler.server_connect(**data)
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
return {}
PortalAdmin.responder(amp_portal_admin)
def call_remote_PortalAdmin(self, sessid, operation="", data=""):
"""
Access method called by the server side.
"""
self.safe_send(PortalAdmin, sessid, operation=operation, data=dumps(data))
# Extra functions
def amp_function_call(self, module, function, args, **kwargs):
"""
This allows Portal- and Server-process to call an arbitrary function
in the other process. It is intended for use by plugin modules.
"""
args = loads(args)
kwargs = loads(kwargs)
# call the function (don't catch tracebacks here)
result = variable_from_module(module, function)(*args, **kwargs)
if isinstance(result, Deferred):
# if result is a deferred, attach handler to properly
# wrap the return value
result.addCallback(lambda r: {"result": dumps(r)})
return result
else:
return {'result': dumps(result)}
FunctionCall.responder(amp_function_call)
def call_remote_FunctionCall(self, modulepath, functionname, *args, **kwargs):
"""
Access method called by either process. This will call an arbitrary
function on the other process (On Portal if calling from Server and
vice versa).
Inputs:
modulepath (str) - python path to module holding function to call
functionname (str) - name of function in given module
*args, **kwargs will be used as arguments/keyword args for the
remote function call
Returns:
A deferred that fires with the return value of the remote
function call
"""
return self.callRemote(FunctionCall,
module=modulepath,
function=functionname,
args=dumps(args),
kwargs=dumps(kwargs)).addCallback(lambda r: loads(r["result"])).addErrback(self.errback, "FunctionCall")

193
lib/server/caches.py Normal file
View file

@ -0,0 +1,193 @@
"""
Central caching module.
"""
from sys import getsizeof
import os
import threading
from collections import defaultdict
from src.server.models import ServerConfig
from src.utils.utils import uses_database, to_str, get_evennia_pids
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
_IS_SUBPROCESS = os.getpid() in get_evennia_pids()
_IS_MAIN_THREAD = threading.currentThread().getName() == "MainThread"
#
# Set up the cache stores
#
_ATTR_CACHE = {}
_PROP_CACHE = defaultdict(dict)
#------------------------------------------------------------
# Cache key hash generation
#------------------------------------------------------------
if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6.4':
# mysql <5.6.4 don't support millisecond precision
_DATESTRING = "%Y:%m:%d-%H:%M:%S:000000"
else:
_DATESTRING = "%Y:%m:%d-%H:%M:%S:%f"
def hashid(obj, suffix=""):
"""
Returns a per-class unique hash that combines the object's
class name with its idnum and creation time. This makes this id unique also
between different typeclassed entities such as scripts and
objects (which may still have the same id).
"""
if not obj:
return obj
try:
hid = _GA(obj, "_hashid")
except AttributeError:
try:
date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id")
except AttributeError:
try:
# maybe a typeclass, try to go to dbobj
obj = _GA(obj, "dbobj")
date, idnum = _GA(obj, "db_date_created").strftime(_DATESTRING), _GA(obj, "id")
except AttributeError:
# this happens if hashing something like ndb. We have to
# rely on memory adressing in this case.
date, idnum = "InMemory", id(obj)
if not idnum or not date:
# this will happen if setting properties on an object which
# is not yet saved
return None
# we have to remove the class-name's space, for eventual use
# of memcached
hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum)
hid = hid.replace(" ", "")
# we cache the object part of the hashid to avoid too many
# object lookups
_SA(obj, "_hashid", hid)
# build the complete hashid
hid = "%s%s" % (hid, suffix)
return to_str(hid)
#------------------------------------------------------------
# Cache callback handlers
#------------------------------------------------------------
# callback to field pre_save signal (connected in src.server.server)
#def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
# """
# Called at the beginning of the field save operation. The save method
# must be called with the update_fields keyword in order to be most efficient.
# This method should NOT save; rather it is the save() that triggers this
# function. Its main purpose is to allow to plug-in a save handler and oob
# handlers.
# """
# if raw:
# return
# if update_fields:
# # this is a list of strings at this point. We want field objects
# update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields)
# else:
# # meta.fields are already field objects; get them all
# update_fields = _GA(_GA(instance, "_meta"), "fields")
# for field in update_fields:
# fieldname = field.name
# handlername = "_at_%s_presave" % fieldname
# handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None
# if callable(handler):
# handler()
def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
"""
Called at the beginning of the field save operation. The save method
must be called with the update_fields keyword in order to be most efficient.
This method should NOT save; rather it is the save() that triggers this
function. Its main purpose is to allow to plug-in a save handler and oob
handlers.
"""
if raw:
return
if update_fields:
# this is a list of strings at this point. We want field objects
update_fields = (_GA(_GA(instance, "_meta"), "get_field_by_name")(field)[0] for field in update_fields)
else:
# meta.fields are already field objects; get them all
update_fields = _GA(_GA(instance, "_meta"), "fields")
for field in update_fields:
fieldname = field.name
handlername = "_at_%s_postsave" % fieldname
handler = _GA(instance, handlername) if handlername in _GA(sender, '__dict__') else None
if callable(handler):
handler()
trackerhandler = _GA(instance, "_trackerhandler") if "_trackerhandler" in _GA(instance, '__dict__') else None
if trackerhandler:
trackerhandler.update(fieldname, _GA(instance, fieldname))
#------------------------------------------------------------
# Attribute lookup cache
#------------------------------------------------------------
def get_attr_cache(obj):
"Retrieve lookup cache"
hid = hashid(obj)
return _ATTR_CACHE.get(hid, None)
def set_attr_cache(obj, store):
"Set lookup cache"
global _ATTR_CACHE
hid = hashid(obj)
_ATTR_CACHE[hid] = store
#------------------------------------------------------------
# Property cache - this is a generic cache for properties stored on models.
#------------------------------------------------------------
# access methods
def get_prop_cache(obj, propname):
"retrieve data from cache"
hid = hashid(obj, "-%s" % propname)
return _PROP_CACHE[hid].get(propname, None) if hid else None
def set_prop_cache(obj, propname, propvalue):
"Set property cache"
hid = hashid(obj, "-%s" % propname)
if hid:
_PROP_CACHE[hid][propname] = propvalue
def del_prop_cache(obj, propname):
"Delete element from property cache"
hid = hashid(obj, "-%s" % propname)
if hid:
if propname in _PROP_CACHE[hid]:
del _PROP_CACHE[hid][propname]
def flush_prop_cache():
"Clear property cache"
global _PROP_CACHE
_PROP_CACHE = defaultdict(dict)
def get_cache_sizes():
"""
Get cache sizes, expressed in number of objects and memory size in MB
"""
global _ATTR_CACHE, _PROP_CACHE
attr_n = len(_ATTR_CACHE)
attr_mb = sum(getsizeof(obj) for obj in _ATTR_CACHE) / 1024.0
prop_n = sum(len(dic) for dic in _PROP_CACHE.values())
prop_mb = sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _PROP_CACHE.values()) / 1024.0
return (attr_n, attr_mb), (prop_n, prop_mb)

267
lib/server/initial_setup.py Normal file
View file

@ -0,0 +1,267 @@
"""
This module handles initial database propagation, which is only run the first
time the game starts. It will create some default channels, objects, and
other things.
Everything starts at handle_setup()
"""
import django
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext as _
from src.players.models import PlayerDB
from src.server.models import ServerConfig
from src.utils import create
from src.utils.utils import class_from_module
def create_config_values():
"""
Creates the initial config values.
"""
ServerConfig.objects.conf("site_name", settings.SERVERNAME)
ServerConfig.objects.conf("idle_timeout", settings.IDLE_TIMEOUT)
def get_god_player():
"""
Creates the god user.
"""
try:
god_player = PlayerDB.objects.get(id=1)
except PlayerDB.DoesNotExist:
txt = "\n\nNo superuser exists yet. The superuser is the 'owner'\n" \
"account on the Evennia server. Create a new superuser using\n" \
"the command\n\n" \
" python manage.py createsuperuser\n\n" \
"Follow the prompts, then restart the server."
raise Exception(txt)
return god_player
def create_objects():
"""
Creates the #1 player and Limbo room.
"""
print " Creating objects (Player #1 and Limbo room) ..."
# Set the initial User's account object's username on the #1 object.
# This object is pure django and only holds name, email and password.
god_player = get_god_player()
# Create a Player 'user profile' object to hold eventual
# mud-specific settings for the PlayerDB object.
player_typeclass = settings.BASE_PLAYER_TYPECLASS
# run all creation hooks on god_player (we must do so manually
# since the manage.py command does not)
god_player.swap_typeclass(player_typeclass, clean_attributes=True)
god_player.basetype_setup()
god_player.at_player_creation()
god_player.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all()")
# this is necessary for quelling to work correctly.
god_player.permissions.add("Immortals")
# Limbo is the default "nowhere" starting room
# Create the in-game god-character for player #1 and set
# it to exist in Limbo.
character_typeclass = settings.BASE_CHARACTER_TYPECLASS
god_character = create.create_object(character_typeclass,
key=god_player.username,
nohome=True)
god_character.id = 1
god_character.db.desc = _('This is User #1.')
god_character.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all();puppet:false()")
god_character.permissions.add("Immortals")
god_character.save()
god_player.attributes.add("_first_login", True)
god_player.attributes.add("_last_puppet", god_character)
god_player.db._playable_characters.append(god_character)
room_typeclass = settings.BASE_ROOM_TYPECLASS
limbo_obj = create.create_object(room_typeclass, _('Limbo'), nohome=True)
limbo_obj.id = 2
string = \
"Welcome to your new {wEvennia{n-based game. From here you are ready " \
"to begin development. Visit http://evennia.com if you should need " \
"help or would like to participate in community discussions. If you " \
"are logged in as user #1 you can create a demo/tutorial area with " \
"{w@batchcommand contrib.tutorial_world.build{n. Use {w@quell{n or login " \
"as normal player to play the demo properly."
string = _(string)
limbo_obj.db.desc = string
limbo_obj.save()
# Now that Limbo exists, try to set the user up in Limbo (unless
# the creation hooks already fixed this).
if not god_character.location:
god_character.location = limbo_obj
if not god_character.home:
god_character.home = limbo_obj
def create_channels():
"""
Creates some sensible default channels.
"""
print " Creating default channels ..."
# public channel
key1, aliases, desc, locks = settings.CHANNEL_PUBLIC
pchan = create.create_channel(key1, aliases, desc, locks=locks)
# mudinfo channel
key2, aliases, desc, locks = settings.CHANNEL_MUDINFO
ichan = create.create_channel(key2, aliases, desc, locks=locks)
# connectinfo channel
key3, aliases, desc, locks = settings.CHANNEL_CONNECTINFO
cchan = create.create_channel(key3, aliases, desc, locks=locks)
# TODO: postgresql-psycopg2 has a strange error when trying to
# connect the user to the default channels. It works fine from inside
# the game, but not from the initial startup. We are temporarily bypassing
# the problem with the following fix. See Evennia Issue 151.
if ((".".join(str(i) for i in django.VERSION) < "1.2"
and settings.DATABASE_ENGINE == "postgresql_psycopg2")
or (hasattr(settings, 'DATABASES')
and settings.DATABASES.get("default", {}).get('ENGINE', None)
== 'django.db.backends.postgresql_psycopg2')):
warning = """
PostgreSQL-psycopg2 compatability fix:
The in-game channels %s, %s and %s were created,
but the superuser was not yet connected to them. Please use in
game commands to onnect Player #1 to those channels when first
logging in.
""" % (key1, key2, key3)
print warning
return
# connect the god user to all these channels by default.
goduser = get_god_player()
pchan.connect(goduser)
ichan.connect(goduser)
cchan.connect(goduser)
def create_system_scripts():
"""
Setup the system repeat scripts. They are automatically started
by the create_script function.
"""
from src.scripts import scripts
print " Creating and starting global scripts ..."
# check so that all sessions are alive.
script1 = create.create_script(scripts.CheckSessions)
# validate all scripts in script table.
script2 = create.create_script(scripts.ValidateScripts)
# update the channel handler to make sure it's in sync
script3 = create.create_script(scripts.ValidateChannelHandler)
# flush the idmapper cache
script4 = create.create_script(scripts.ValidateIdmapperCache)
if not script1 or not script2 or not script3 or not script4:
print " Error creating system scripts."
def start_game_time():
"""
This starts a persistent script that keeps track of the
in-game time (in whatever accelerated reference frame), but also
the total run time of the server as well as its current uptime
(the uptime can also be found directly from the server though).
"""
print " Starting in-game time ..."
from src.utils import gametime
gametime.init_gametime()
def at_initial_setup():
"""
Custom hook for users to overload some or all parts of the initial
setup. Called very last in the sequence. It tries to import and
srun a module settings.AT_INITIAL_SETUP_HOOK_MODULE and will fail
silently if this does not exist or fails to load.
"""
modname = settings.AT_INITIAL_SETUP_HOOK_MODULE
if not modname:
return
try:
mod = __import__(modname, fromlist=[None])
except (ImportError, ValueError):
return
print " Running at_initial_setup() hook."
if mod.__dict__.get("at_initial_setup", None):
mod.at_initial_setup()
def reset_server():
"""
We end the initialization by resetting the server. This
makes sure the first login is the same as all the following
ones, particularly it cleans all caches for the special objects.
It also checks so the warm-reset mechanism works as it should.
"""
from src.server.sessionhandler import SESSIONS
print " Initial setup complete. Restarting Server once."
SESSIONS.server.shutdown(mode='reset')
def handle_setup(last_step):
"""
Main logic for the module. It allows for restarting
the initialization at any point if one of the modules
should crash.
"""
if last_step < 0:
# this means we don't need to handle setup since
# it already ran sucessfully once.
return
elif last_step is None:
# config doesn't exist yet. First start of server
last_step = 0
# setting up the list of functions to run
setup_queue = [
create_config_values,
create_objects,
create_channels,
create_system_scripts,
start_game_time,
at_initial_setup,
reset_server
]
#print " Initial setup: %s steps." % (len(setup_queue))
# step through queue, from last completed function
for num, setup_func in enumerate(setup_queue[last_step:]):
# run the setup function. Note that if there is a
# traceback we let it stop the system so the config
# step is not saved.
#print "%s..." % num
try:
setup_func()
except Exception:
if last_step + num == 2:
from src.players.models import PlayerDB
from src.objects.models import ObjectDB
for obj in ObjectDB.objects.all():
obj.delete()
for profile in PlayerDB.objects.all():
profile.delete()
elif last_step + num == 3:
from src.comms.models import ChannelDB
ChannelDB.objects.all().delete()
raise
ServerConfig.objects.conf("last_initial_setup_step", last_step + num + 1)
# We got through the entire list. Set last_step to -1 so we don't
# have to run this again.
ServerConfig.objects.conf("last_initial_setup_step", -1)

53
lib/server/manager.py Normal file
View file

@ -0,0 +1,53 @@
"""
Custom manager for ServerConfig objects.
"""
from django.db import models
class ServerConfigManager(models.Manager):
"""
This ServerConfigManager implements methods for searching
and manipulating ServerConfigs directly from the database.
These methods will all return database objects
(or QuerySets) directly.
ServerConfigs are used to store certain persistent settings for the
server at run-time.
Evennia-specific:
conf
"""
def conf(self, key=None, value=None, delete=False, default=None):
"""
Access and manipulate config values
"""
if not key:
return self.all()
elif delete is True:
for conf in self.filter(db_key=key):
conf.delete()
elif value is not None:
conf = self.filter(db_key=key)
if conf:
conf = conf[0]
else:
conf = self.model(db_key=key)
conf.value = value # this will pickle
else:
conf = self.filter(db_key=key)
if not conf:
return default
return conf[0].value
def get_mysql_db_version(self):
"""
This is a helper method for getting the version string
of a mysql database.
"""
from django.db import connection
conn = connection.cursor()
conn.execute("SELECT VERSION()")
version = conn.fetchone()
return version and str(version[0]) or ""

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='ServerConfig',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_key', models.CharField(unique=True, max_length=64)),
('db_value', models.TextField(blank=True)),
],
options={
'verbose_name': 'Server Config value',
'verbose_name_plural': 'Server Config values',
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

118
lib/server/models.py Normal file
View file

@ -0,0 +1,118 @@
"""
Server Configuration flags
This holds persistent server configuration flags.
Config values should usually be set through the
manager's conf() method.
"""
try:
import cPickle as pickle
except ImportError:
import pickle
from django.db import models
from src.utils.idmapper.models import WeakSharedMemoryModel
from src.utils import logger, utils
from src.server.manager import ServerConfigManager
#------------------------------------------------------------
#
# ServerConfig
#
#------------------------------------------------------------
class ServerConfig(WeakSharedMemoryModel):
"""
On-the fly storage of global settings.
Properties defined on ServerConfig:
key - main identifier
value - value stored in key. This is a pickled storage.
"""
#
# ServerConfig database model setup
#
#
# These database fields are all set using their corresponding properties,
# named same as the field, but withtout the db_* prefix.
# main name of the database entry
db_key = models.CharField(max_length=64, unique=True)
# config value
db_value = models.TextField(blank=True)
objects = ServerConfigManager()
_is_deleted = False
# Wrapper properties to easily set database fields. These are
# @property decorators that allows to access these fields using
# normal python operations (without having to remember to save()
# etc). So e.g. a property 'attr' has a get/set/del decorator
# defined that allows the user to do self.attr = value,
# value = self.attr and del self.attr respectively (where self
# is the object in question).
# key property (wraps db_key)
#@property
def __key_get(self):
"Getter. Allows for value = self.key"
return self.db_key
#@key.setter
def __key_set(self, value):
"Setter. Allows for self.key = value"
self.db_key = value
self.save()
#@key.deleter
def __key_del(self):
"Deleter. Allows for del self.key. Deletes entry."
self.delete()
key = property(__key_get, __key_set, __key_del)
# value property (wraps db_value)
#@property
def __value_get(self):
"Getter. Allows for value = self.value"
return pickle.loads(str(self.db_value))
#@value.setter
def __value_set(self, value):
"Setter. Allows for self.value = value"
if utils.has_parent('django.db.models.base.Model', value):
# we have to protect against storing db objects.
logger.log_errmsg("ServerConfig cannot store db objects! (%s)" % value)
return
self.db_value = pickle.dumps(value)
self.save()
#@value.deleter
def __value_del(self):
"Deleter. Allows for del self.value. Deletes entry."
self.delete()
value = property(__value_get, __value_set, __value_del)
class Meta:
"Define Django meta options"
verbose_name = "Server Config value"
verbose_name_plural = "Server Config values"
#
# ServerConfig other methods
#
def __unicode__(self):
return "%s : %s" % (self.key, self.value)
def store(self, key, value):
"""
Wrap the storage (handles pickling)
"""
self.key = key
self.value = value

221
lib/server/oob_cmds.py Normal file
View file

@ -0,0 +1,221 @@
"""
Out-of-band default plugin commands available for OOB handler.
This module implements commands as defined by the MSDP standard
(http://tintin.sourceforge.net/msdp/), but is independent of the
actual transfer protocol (webclient, MSDP, GMCP etc).
This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions
(not classes) defined globally in this module will be made available
to the oob mechanism.
oob functions have the following call signature:
function(oobhandler, session, *args, **kwargs)
where oobhandler is a back-reference to the central OOB_HANDLER
instance and session is the active session to get return data.
The function names are not case-sensitive (this allows for names
like "LIST" which would otherwise collide with Python builtins).
A function named OOB_ERROR will retrieve error strings if it is
defined. It will get the error message as its 3rd argument.
Data is usually returned via
session.msg(oob=(cmdname, (args,), {kwargs}))
Note that args, kwargs must be iterable/dict, non-iterables will
be interpreted as a new command name.
"""
from django.conf import settings
_GA = object.__getattribute__
_SA = object.__setattr__
_NA_SEND = lambda o: "N/A"
#------------------------------------------------------------
# All OOB commands must be on the form
# cmdname(oobhandler, session, *args, **kwargs)
#------------------------------------------------------------
def OOB_ERROR(oobhandler, session, errmsg, *args, **kwargs):
"""
A function with this name is special and is called by the oobhandler when an error
occurs already at the execution stage (such as the oob function
not being recognized or having the wrong args etc).
"""
session.msg(oob=("err", ("ERROR " + errmsg,)))
def ECHO(oobhandler, session, *args, **kwargs):
"Test/debug function, simply returning the args and kwargs"
session.msg(oob=("echo", args, kwargs))
##OOB{"SEND":"CHARACTER_NAME"}
def SEND(oobhandler, session, *args, **kwargs):
"""
This function directly returns the value of the given variable to the
session.
"""
obj = session.get_puppet_or_player()
ret = {}
if obj:
for name in (a.upper() for a in args if a):
try:
value = OOB_SENDABLE.get(name, _NA_SEND)(obj)
ret[name] = value
except Exception, e:
ret[name] = str(e)
session.msg(oob=("send", ret))
else:
session.msg(oob=("err", ("You must log in first.",)))
##OOB{"REPORT":"TEST"}
def REPORT(oobhandler, session, *args, **kwargs):
"""
This creates a tracker instance to track the data given in *args.
The tracker will return with a oob structure
oob={"report":["attrfieldname", (args,), {kwargs}}
Note that the data name is assumed to be a field is it starts with db_*
and an Attribute otherwise.
"Example of tracking changes to the db_key field and the desc" Attribite:
REPORT(oobhandler, session, "CHARACTER_NAME", )
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
trackname = OOB_REPORTABLE.get(name, None)
if not trackname:
session.msg(oob=("err", ("No Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % trackname,)))
elif trackname.startswith("db_"):
oobhandler.track_field(obj, session.sessid, trackname)
else:
oobhandler.track_attribute(obj, session.sessid, trackname)
else:
session.msg(oob=("err", ("You must log in first.",)))
##OOB{"UNREPORT": "TEST"}
def UNREPORT(oobhandler, session, *args, **kwargs):
"""
This removes tracking for the given data given in *args.
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
trackname = OOB_REPORTABLE.get(name, None)
if not trackname:
session.msg(oob=("err", ("No Un-Reportable property '%s'. Use LIST REPORTED_VALUES." % name,)))
elif trackname.startswith("db_"):
oobhandler.untrack_field(obj, session.sessid, trackname)
else: # assume attribute
oobhandler.untrack_attribute(obj, session.sessid, trackname)
else:
session.msg(oob=("err", ("You must log in first.",)))
##OOB{"LIST":"COMMANDS"}
def LIST(oobhandler, session, mode, *args, **kwargs):
"""
List available properties. Mode is the type of information
desired:
"COMMANDS" Request an array of commands supported
by the server.
"LISTS" Request an array of lists supported
by the server.
"CONFIGURABLE_VARIABLES" Request an array of variables the client
can configure.
"REPORTABLE_VARIABLES" Request an array of variables the server
will report.
"REPORTED_VARIABLES" Request an array of variables currently
being reported.
"SENDABLE_VARIABLES" Request an array of variables the server
will send.
"""
mode = mode.upper()
if mode == "COMMANDS":
session.msg(oob=("list", ("COMMANDS",
"LIST",
"REPORT",
"UNREPORT",
# "RESET",
"SEND")))
elif mode == "LISTS":
session.msg(oob=("list", ("LISTS",
"REPORTABLE_VARIABLES",
"REPORTED_VARIABLES",
# "CONFIGURABLE_VARIABLES",
"SENDABLE_VARIABLES")))
elif mode == "REPORTABLE_VARIABLES":
session.msg(oob=("list", ("REPORTABLE_VARIABLES",) +
tuple(key for key in OOB_REPORTABLE.keys())))
elif mode == "REPORTED_VARIABLES":
# we need to check so as to use the right return value depending on if it is
# an Attribute (identified by tracking the db_value field) or a normal database field
reported = oobhandler.get_all_tracked(session)
reported = [stored[2] if stored[2] != "db_value" else stored[4][0] for stored in reported]
session.msg(oob=("list", ["REPORTED_VARIABLES"] + reported))
elif mode == "SENDABLE_VARIABLES":
session.msg(oob=("list", ("SENDABLE_VARIABLES",) +
tuple(key for key in OOB_REPORTABLE.keys())))
elif mode == "CONFIGURABLE_VARIABLES":
# Not implemented (game specific)
pass
else:
session.msg(oob=("err", ("LIST", "Unsupported mode",)))
def _repeat_callback(oobhandler, session, *args, **kwargs):
"Set up by REPEAT"
session.msg(oob=("repeat", ("Repeat!",)))
##OOB{"REPEAT":10}
def REPEAT(oobhandler, session, interval, *args, **kwargs):
"""
Test command for the repeat functionality. Note that the args/kwargs
must not be db objects (or anything else non-picklable), rather use
dbrefs if so needed. The callback must be defined globally and
will be called as
callback(oobhandler, session, *args, **kwargs)
"""
oobhandler.repeat(None, session.sessid, interval, _repeat_callback, *args, **kwargs)
##OOB{"UNREPEAT":10}
def UNREPEAT(oobhandler, session, interval):
"""
Disable repeating callback
"""
oobhandler.unrepeat(None, session.sessid, interval)
# Mapping for how to retrieve each property name.
# Each entry should point to a callable that gets the interesting object as
# input and returns the relevant value.
# MSDP recommends the following standard name mappings for general compliance:
# "CHARACTER_NAME", "SERVER_ID", "SERVER_TIME", "AFFECTS", "ALIGNMENT", "EXPERIENCE", "EXPERIENCE_MAX", "EXPERIENCE_TNL",
# "HEALTH", "HEALTH_MAX", "LEVEL", "RACE", "CLASS", "MANA", "MANA_MAX", "WIMPY", "PRACTICE", "MONEY", "MOVEMENT",
# "MOVEMENT_MAX", "HITROLL", "DAMROLL", "AC", "STR", "INT", "WIS", "DEX", "CON", "OPPONENT_HEALTH", "OPPONENT_HEALTH_MAX",
# "OPPONENT_LEVEL", "OPPONENT_NAME", "AREA_NAME", "ROOM_EXITS", "ROOM_VNUM", "ROOM_NAME", "WORLD_TIME", "CLIENT_ID",
# "CLIENT_VERSION", "PLUGIN_ID", "ANSI_COLORS", "XTERM_256_COLORS", "UTF_8", "SOUND", "MXP", "BUTTON_1", "BUTTON_2",
# "BUTTON_3", "BUTTON_4", "BUTTON_5", "GAUGE_1", "GAUGE_2","GAUGE_3", "GAUGE_4", "GAUGE_5"
OOB_SENDABLE = {
"CHARACTER_NAME": lambda o: o.key,
"SERVER_ID": lambda o: settings.SERVERNAME,
"ROOM_NAME": lambda o: o.db_location.key,
"ANSI_COLORS": lambda o: True,
"XTERM_256_COLORS": lambda o: True,
"UTF_8": lambda o: True
}
# mapping for which properties may be tracked. Each value points either to a database field
# (starting with db_*) or an Attribute name.
OOB_REPORTABLE = {
"CHARACTER_NAME": "db_key",
"ROOM_NAME": "db_location",
"TEST" : "test"
}

401
lib/server/oobhandler.py Normal file
View file

@ -0,0 +1,401 @@
"""
OOBHandler - Out Of Band Handler
The OOBHandler.execute_cmd is called by the sessionhandler when it detects
an OOB instruction (exactly how this looked depends on the protocol; at this
point all oob calls should look the same)
The handler pieces of functionality:
function execution - the oob protocol can execute a function directly on
the server. The available functions must be defined
as global functions in settings.OOB_PLUGIN_MODULES.
repeat func execution - the oob protocol can request a given function be
executed repeatedly at a regular interval. This
uses an internal script pool.
tracking - the oob protocol can request Evennia to track changes to
fields on objects, as well as changes in Attributes. This is
done by dynamically adding tracker-objects on entities. The
behaviour of those objects can be customized by adding new
tracker classes in settings.OOB_PLUGIN_MODULES.
What goes into the OOB_PLUGIN_MODULES is a (list of) modules that contains
the working server-side code available to the OOB system: oob functions and
tracker classes.
oob functions have the following call signature:
function(caller, session, *args, **kwargs)
oob trackers should inherit from the OOBTracker class (in this
module) and implement a minimum of the same functionality.
If a function named "oob_error" is given, this will be called with error
messages.
"""
from inspect import isfunction
from twisted.internet.defer import inlineCallbacks
from django.conf import settings
from src.server.models import ServerConfig
from src.server.sessionhandler import SESSIONS
#from src.scripts.scripts import Script
#from src.utils.create import create_script
from src.scripts.tickerhandler import Ticker, TickerPool, TickerHandler
from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
from src.utils import logger
from src.utils.utils import all_from_module, make_iter, to_str
_SA = object.__setattr__
_GA = object.__getattribute__
_DA = object.__delattr__
# load resources from plugin module
_OOB_FUNCS = {}
for mod in make_iter(settings.OOB_PLUGIN_MODULES):
_OOB_FUNCS.update(dict((key.lower(), func) for key, func in all_from_module(mod).items() if isfunction(func)))
# get custom error method or use the default
_OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
if not _OOB_ERROR:
# create default oob error message function
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
"Error wrapper"
session.msg(oob=("err", ("ERROR ", errmsg)))
_OOB_ERROR = oob_error
#
# TrackerHandler is assigned to objects that should notify themselves to
# the OOB system when some property changes. This is never assigned manually
# but automatically through the OOBHandler.
#
class TrackerHandler(object):
"""
This object is dynamically assigned to objects whenever one of its fields
are to be tracked. It holds an internal dictionary mapping to the fields
on that object. Each field can be tracked by any number of trackers (each
tied to a different callback).
"""
def __init__(self, obj):
"""
This is initiated and stored on the object as a
property _trackerhandler.
"""
self.obj = obj
self.ntrackers = 0
# initiate store only with valid on-object fieldnames
self.tracktargets = dict((key, {})
for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")())
def add(self, fieldname, tracker):
"""
Add tracker to the handler. Raises KeyError if fieldname
does not exist.
"""
trackerkey = tracker.__class__.__name__
self.tracktargets[fieldname][trackerkey] = tracker
self.ntrackers += 1
def remove(self, fieldname, trackerclass, *args, **kwargs):
"""
Remove identified tracker from TrackerHandler.
Raises KeyError if tracker is not found.
"""
trackerkey = trackerclass.__name__
tracker = self.tracktargets[fieldname][trackerkey]
try:
tracker.at_remove(*args, **kwargs)
except Exception:
logger.log_trace()
del self.tracktargets[fieldname][trackerkey]
self.ntrackers -= 1
if self.ntrackers <= 0:
# if there are no more trackers, clean this handler
del self
def update(self, fieldname, new_value):
"""
Called by the field when it updates to a new value
"""
for tracker in self.tracktargets[fieldname].values():
try:
tracker.update(new_value)
except Exception:
logger.log_trace()
# On-object Trackers to load with TrackerHandler
class TrackerBase(object):
"""
Base class for OOB Tracker objects. Inherit from this
to define custom trackers.
"""
def __init__(self, *args, **kwargs):
pass
def update(self, *args, **kwargs):
"Called by tracked objects"
pass
def at_remove(self, *args, **kwargs):
"Called when tracker is removed"
pass
class ReportFieldTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
a named database field changes. The TrackerHandler calls this with
the correct arguments.
"""
def __init__(self, oobhandler, fieldname, sessid, *args, **kwargs):
"""
name - name of entity to track, such as "db_key"
sessid - sessid of session to report to
"""
self.oobhandler = oobhandler
self.fieldname = fieldname
self.sessid = sessid
def update(self, new_value, *args, **kwargs):
"Called by cache when updating the tracked entitiy"
# use oobhandler to relay data
try:
# we must never relay objects across the amp, only text data.
new_value = new_value.key
except AttributeError:
new_value = to_str(new_value, force_string=True)
kwargs[self.fieldname] = new_value
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", *args, **kwargs)
class ReportAttributeTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
the Attribute updates. Since the field here is always "db_key",
we instead store the name of the attribute to return.
"""
def __init__(self, oobhandler, fieldname, sessid, attrname, *args, **kwargs):
"""
attrname - name of attribute to track
sessid - sessid of session to report to
"""
self.oobhandler = oobhandler
self.attrname = attrname
self.sessid = sessid
def update(self, new_value, *args, **kwargs):
"Called by cache when attribute's db_value field updates"
kwargs[self.attrname] = new_value
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", *args, **kwargs)
# Ticker of auto-updating objects
class OOBTicker(Ticker):
"""
Version of Ticker that executes an executable rather than trying to call
a hook method.
"""
@inlineCallbacks
def _callback(self):
"See original for more info"
for key, (_, args, kwargs) in self.subscriptions.items():
# args = (sessid, callback_function)
session = SESSIONS.session_from_sessid(args[0])
try:
# execute the oob callback
yield args[1](OOB_HANDLER, session, *args[2:], **kwargs)
except Exception:
logger.log_trace()
class OOBTickerPool(TickerPool):
ticker_class = OOBTicker
class OOBTickerHandler(TickerHandler):
ticker_pool_class = OOBTickerPool
# Main OOB Handler
class OOBHandler(object):
"""
The OOBHandler maintains all dynamic on-object oob hooks. It will store the
creation instructions and and re-apply them at a server reload (but
not after a server shutdown)
"""
def __init__(self):
"""
Initialize handler
"""
self.sessionhandler = SESSIONS
self.oob_tracker_storage = {}
self.tickerhandler = OOBTickerHandler("oob_ticker_storage")
def save(self):
"""
Save the command_storage as a serialized string into a temporary
ServerConf field
"""
if self.oob_tracker_storage:
#print "saved tracker_storage:", self.oob_tracker_storage
ServerConfig.objects.conf(key="oob_tracker_storage",
value=dbserialize(self.oob_tracker_storage))
self.tickerhandler.save()
def restore(self):
"""
Restore the command_storage from database and re-initialize the handler from storage.. This is
only triggered after a server reload, not after a shutdown-restart
"""
# load stored command instructions and use them to re-initialize handler
tracker_storage = ServerConfig.objects.conf(key="oob_tracker_storage")
if tracker_storage:
self.oob_tracker_storage = dbunserialize(tracker_storage)
for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values():
#print "restoring tracking:",obj, sessid, fieldname, trackerclass
self._track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs)
# make sure to purge the storage
ServerConfig.objects.conf(key="oob_tracker_storage", delete=True)
self.tickerhandler.restore()
def _track(self, obj, sessid, propname, trackerclass, *args, **kwargs):
"""
Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args,
kwargs will be used to initialize the OOB hook before adding
it to obj.
If propname is not given, but the OOB has a class property
named as propname, this will be used as the property name when assigning
the OOB to obj, otherwise tracker_key is used as the property name.
"""
if not "_trackerhandler" in _GA(obj, "__dict__"):
# assign trackerhandler to object
_SA(obj, "_trackerhandler", TrackerHandler(obj))
# initialize object
tracker = trackerclass(self, propname, sessid, *args, **kwargs)
_GA(obj, "_trackerhandler").add(propname, tracker)
# store calling arguments as a pickle for retrieval later
obj_packed = pack_dbobj(obj)
storekey = (obj_packed, sessid, propname)
stored = (obj_packed, sessid, propname, trackerclass, args, kwargs)
self.oob_tracker_storage[storekey] = stored
#print "_track:", obj, id(obj), obj.__dict__
def _untrack(self, obj, sessid, propname, trackerclass, *args, **kwargs):
"""
Remove the OOB from obj. If oob implements an
at_delete hook, this will be called with args, kwargs
"""
try:
# call at_remove hook on the trackerclass
_GA(obj, "_trackerhandler").remove(propname, trackerclass, *args, **kwargs)
except AttributeError:
pass
# remove the pickle from storage
store_key = (pack_dbobj(obj), sessid, propname)
self.oob_tracker_storage.pop(store_key, None)
def get_all_tracked(self, session):
"""
Get the names of all variables this session is tracking.
"""
sessid = session.sessid
return [stored for key, stored in self.oob_tracker_storage.items() if key[1] == sessid]
def track_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker):
"""
Shortcut wrapper method for specifically tracking a database field.
Takes the tracker class as argument.
"""
# all database field names starts with db_*
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
self._track(obj, sessid, field_name, trackerclass, field_name)
def untrack_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker):
"""
Shortcut for untracking a database field. Uses OOBTracker by defualt
"""
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
self._untrack(obj, sessid, field_name, trackerclass)
def track_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker):
"""
Shortcut wrapper method for specifically tracking the changes of an
Attribute on an object. Will create a tracker on the Attribute
Object and name in a way the Attribute expects.
"""
# get the attribute object if we can
attrobj = obj.attributes.get(attr_name, return_obj=True)
#print "track_attribute attrobj:", attrobj, id(attrobj)
if attrobj:
self._track(attrobj, sessid, "db_value", trackerclass, attr_name)
def untrack_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker):
"""
Shortcut for deactivating tracking for a given attribute.
"""
attrobj = obj.attributes.get(attr_name, return_obj=True)
if attrobj:
self._untrack(attrobj, sessid, "db_value", trackerclass, attr_name)
def repeat(self, obj, sessid, interval=20, callback=None, *args, **kwargs):
"""
Start a repeating action. Every interval seconds, trigger
callback(*args, **kwargs). The callback is called with
args and kwargs; note that *args and **kwargs may not contain
anything un-picklable (use dbrefs if wanting to use objects).
"""
self.tickerhandler.add(obj, interval, sessid, callback, *args, **kwargs)
def unrepeat(self, obj, sessid, interval=20):
"""
Stop a repeating action
"""
self.tickerhandler.remove(obj, interval)
# access method - called from session.msg()
def execute_cmd(self, session, func_key, *args, **kwargs):
"""
Retrieve oobfunc from OOB_FUNCS and execute it immediately
using *args and **kwargs
"""
oobfunc = _OOB_FUNCS.get(func_key, None)
if not oobfunc:
# function not found
errmsg = "OOB Error: function '%s' not recognized." % func_key
if _OOB_ERROR:
_OOB_ERROR(self, session, errmsg, *args, **kwargs)
logger.log_trace()
else:
logger.log_trace(errmsg)
return
# execute the found function
try:
#print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
oobfunc(self, session, *args, **kwargs)
except Exception, err:
errmsg = "OOB Error: Exception in '%s'(%s, %s):\n%s" % (func_key, args, kwargs, err)
if _OOB_ERROR:
_OOB_ERROR(self, session, errmsg, *args, **kwargs)
logger.log_trace(errmsg)
raise Exception(errmsg)
def msg(self, sessid, funcname, *args, **kwargs):
"Shortcut to force-send an OOB message through the oobhandler to a session"
session = self.sessionhandler.session_from_sessid(sessid)
#print "oobhandler msg:", sessid, session, funcname, args, kwargs
if session:
session.msg(oob=(funcname, args, kwargs))
# access object
OOB_HANDLER = OOBHandler()

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

399
lib/server/portal/imc2.py Normal file
View file

@ -0,0 +1,399 @@
"""
IMC2 client module. Handles connecting to and communicating with an IMC2 server.
"""
from time import time
from twisted.internet import task
from twisted.application import internet
from twisted.internet import protocol
from twisted.conch import telnet
from src.server.session import Session
from src.utils import logger, utils
from src.server.portal.imc2lib import imc2_ansi
from src.server.portal.imc2lib import imc2_packets as pck
from django.utils.translation import ugettext as _
# storage containers for IMC2 muds and channels
class IMC2Mud(object):
"""
Stores information about other games connected to our current IMC2 network.
"""
def __init__(self, packet):
self.name = packet.origin
self.versionid = packet.optional_data.get('versionid', None)
self.networkname = packet.optional_data.get('networkname', None)
self.url = packet.optional_data.get('url', None)
self.host = packet.optional_data.get('host', None)
self.port = packet.optional_data.get('port', None)
self.sha256 = packet.optional_data.get('sha256', None)
# This is used to determine when a Mud has fallen into inactive status.
self.last_updated = time()
class IMC2MudList(dict):
"""
Keeps track of other MUDs connected to the IMC network.
"""
def get_mud_list(self):
"""
Returns a sorted list of connected Muds.
"""
muds = self.items()
muds.sort()
return [value for key, value in muds]
def update_mud_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
Mud list for later retrieval.
"""
mud = IMC2Mud(packet)
self[mud.name] = mud
def remove_mud_from_packet(self, packet):
"""
Removes a mud from the Mud list when given a packet.
"""
mud = IMC2Mud(packet)
try:
del self[mud.name]
except KeyError:
# No matching entry, no big deal.
pass
class IMC2Channel(object):
"""
Stores information about channels available on the network.
"""
def __init__(self, packet):
self.localname = packet.optional_data.get('localname', None)
self.name = packet.optional_data.get('channel', None)
self.level = packet.optional_data.get('level', None)
self.owner = packet.optional_data.get('owner', None)
self.policy = packet.optional_data.get('policy', None)
self.last_updated = time()
class IMC2ChanList(dict):
"""
Keeps track of Channels on the IMC network.
"""
def get_channel_list(self):
"""
Returns a sorted list of cached channels.
"""
channels = self.items()
channels.sort()
return [value for key, value in channels]
def update_channel_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
channel list for later retrieval.
"""
channel = IMC2Channel(packet)
self[channel.name] = channel
def remove_channel_from_packet(self, packet):
"""
Removes a channel from the Channel list when given a packet.
"""
channel = IMC2Channel(packet)
try:
del self[channel.name]
except KeyError:
# No matching entry, no big deal.
pass
#
# IMC2 protocol
#
class IMC2Bot(telnet.StatefulTelnetProtocol, Session):
"""
Provides the abstraction for the IMC2 protocol. Handles connection,
authentication, and all necessary packets.
"""
def __init__(self):
self.is_authenticated = False
# only support plaintext passwords
self.auth_type = "plaintext"
self.sequence = None
self.imc2_mudlist = IMC2MudList()
self.imc2_chanlist = IMC2ChanList()
def _send_packet(self, packet):
"Helper function to send packets across the wire"
packet.imc2_protocol = self
packet_str = utils.to_str(packet.assemble(self.factory.mudname,
self.factory.client_pwd, self.factory.server_pwd))
self.sendLine(packet_str)
def _isalive(self):
"Send an isalive packet"
self._send_packet(pck.IMC2PacketIsAlive())
def _keepalive(self):
"Send a keepalive packet"
# send to channel?
self._send_packet(pck.IMC2PacketKeepAliveRequest())
def _channellist(self):
"Sync the network channel list"
checked_networks = []
if not self.network in checked_networks:
self._send_packet(pck.IMC2PacketIceRefresh())
checked_networks.append(self.network)
def _prune(self):
"Prune active channel list"
t0 = time()
for name, mudinfo in self.imc2_mudlist.items():
if t0 - mudinfo.last_updated > 3599:
del self.imc2_mudlist[name]
def _whois_reply(self, packet):
"handle reply from server from an imcwhois request"
# packet.target potentially contains the id of an character to target
# not using that here
response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown'))
string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text}
# somehow pass reply on to a given player, for now we just send to channel
self.data_in(string)
def _format_tell(self, packet):
"""
Handle tells over IMC2 by formatting the text properly
"""
return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender": packet.sender,
"origin": packet.origin,
"msg": packet.optional_data.get('text', 'ERROR: No text provided.')}
def _imc_login(self, line):
"Connect and identify to imc network"
if self.auth_type == "plaintext":
# Only support Plain text passwords.
# SERVER Sends: PW <servername> <serverpw> version=<version#> <networkname>
logger.log_infomsg("IMC2: AUTH< %s" % line)
line_split = line.split(' ')
pw_present = line_split[0] == 'PW'
autosetup_present = line_split[0] == 'autosetup'
if "reject" in line_split:
auth_message = _("IMC2 server rejected connection.")
logger.log_infomsg(auth_message)
return
if pw_present:
self.server_name = line_split[1]
self.network_name = line_split[4]
elif autosetup_present:
logger.log_infomsg(_("IMC2: Autosetup response found."))
self.server_name = line_split[1]
self.network_name = line_split[3]
self.is_authenticated = True
self.sequence = int(time())
# Log to stdout and notify over MUDInfo.
logger.log_infomsg('IMC2: Authenticated to %s' % self.factory.network)
# Ask to see what other MUDs are connected.
self._send_packet(pck.IMC2PacketKeepAliveRequest())
# IMC2 protocol states that KeepAliveRequests should be followed
# up by the requester sending an IsAlive packet.
self._send_packet(pck.IMC2PacketIsAlive())
# Get a listing of channels.
self._send_packet(pck.IMC2PacketIceRefresh())
def connectionMade(self):
"""
Triggered after connecting to the IMC2 network.
"""
self.stopping = False
self.factory.bot = self
address = "%s@%s" % (self.mudname, self.network)
self.init_session("ircbot", address, self.factory.sessionhandler)
# link back and log in
self.uid = int(self.factory.uid)
self.logged_in = True
self.factory.sessionhandler.connect(self)
logger.log_infomsg("IMC2 bot connected to %s." % self.network)
# Send authentication packet. The reply will be caught by lineReceived
self._send_packet(pck.IMC2PacketAuthPlaintext())
def lineReceived(self, line):
"""
IMC2 -> Evennia
Triggered when text is received from the IMC2 network. Figures out
what to do with the packet. This deals with the following
"""
line = line.strip()
if not self.is_authenticated:
# we are not authenticated yet. Deal with this.
self._imc_login(line)
return
#logger.log_infomsg("IMC2: RECV> %s" % line)
# Parse the packet and encapsulate it for easy access
packet = pck.IMC2Packet(self.mudname, packet_str=line)
# Figure out what kind of packet we're dealing with and hand it
# off to the correct handler.
if packet.packet_type == 'is-alive':
self.imc2_mudlist.update_mud_from_packet(packet)
elif packet.packet_type == 'keepalive-request':
# Don't need to check the destination, we only receive these
# packets when they are intended for us.
self.send_packet(pck.IMC2PacketIsAlive())
elif packet.packet_type == 'ice-msg-b':
self.data_out(text=line, packettype="broadcast")
elif packet.packet_type == 'whois-reply':
# handle eventual whois reply
self._whois_reply(packet)
elif packet.packet_type == 'close-notify':
self.imc2_mudlist.remove_mud_from_packet(packet)
elif packet.packet_type == 'ice-update':
self.imc2_chanlist.update_channel_from_packet(packet)
elif packet.packet_type == 'ice-destroy':
self.imc2_chanlist.remove_channel_from_packet(packet)
elif packet.packet_type == 'tell':
# send message to identified player
pass
def data_in(self, text=None, **kwargs):
"""
Data IMC2 -> Evennia
"""
text = "bot_data_in " + text
self.sessionhandler.data_in(self, text=text, **kwargs)
def data_out(self, text=None, **kwargs):
"""
Evennia -> IMC2
Keywords
packet_type:
broadcast - send to everyone on IMC channel
tell - send a tell (see target keyword)
whois - get whois information (see target keyword)
sender - used by tell to identify the sender
target - key identifier of target to tells or whois. If not
given "Unknown" will be used.
destination - used by tell to specify mud destination to send to
"""
if self.sequence:
# This gets incremented with every command.
self.sequence += 1
packet_type = kwargs.get("packet_type", "imcbroadcast")
if packet_type == "broadcast":
# broadcast to everyone on IMC channel
if text.startswith("bot_data_out"):
text = text.split(" ", 1)[1]
else:
return
# we remove the extra channel info since imc2 supplies this anyway
if ":" in text:
header, message = [part.strip() for part in text.split(":", 1)]
# Create imc2packet and send it
self._send_packet(pck.IMC2PacketIceMsgBroadcasted(self.servername,
self.channel,
header, text))
elif packet_type == "tell":
# send an IMC2 tell
sender = kwargs.get("sender", self.mudname)
target = kwargs.get("target", "Unknown")
destination = kwargs.get("destination", "Unknown")
self._send_packet(pck.IMC2PacketTell(sender, target, destination, text))
elif packet_type == "whois":
# send a whois request
sender = kwargs.get("sender", self.mudname)
target = kwargs.get("target", "Unknown")
self._send_packet(pck.IMC2PacketWhois(sender, target))
class IMC2BotFactory(protocol.ReconnectingClientFactory):
"""
Creates instances of the IMC2Protocol. Should really only ever
need to create one connection. Tied in via src/server.py.
"""
initialDelay = 1
factor = 1.5
maxDelay = 60
def __init__(self, sessionhandler, uid=None, network=None, channel=None,
port=None, mudname=None, client_pwd=None, server_pwd=None):
self.uid = uid
self.network = network
sname, host = network.split(".", 1)
self.servername = sname.strip()
self.channel = channel
self.port = port
self.mudname = mudname
self.protocol_version = '2'
self.client_pwd = client_pwd
self.server_pwd = server_pwd
self.bot = None
self.task_isalive = None
self.task_keepalive = None
self.task_prune = None
self.task_channellist = None
def buildProtocol(self, addr):
"Build the protocol"
protocol = IMC2Bot()
protocol.factory = self
protocol.network = self.network
protocol.servername = self.servername
protocol.channel = self.channel
protocol.mudname = self.mudname
protocol.port = self.port
return protocol
def clientConnectionFailed(self, connector, reason):
self.retry(connector)
def clientConnectionLost(self, connector, reason):
if not self.bot.stopping:
self.retry(connector)
def start(self):
"Connect session to sessionhandler"
def errback(fail):
logger.log_errmsg(fail.value)
if self.port:
service = internet.TCPClient(self.network, int(self.port), self)
self.sessionhandler.portal.services.addService(service)
# start tasks
self.task_isalive = task.LoopingCall(self.bot._isalive)
self.task_keepalive = task.LoopingCall(self.bot._keepalive)
self.task_prune = task.LoopingCall(self.bot._prune)
self.task_channellist = task.LoopingCall(self.bot._channellist)
self.task_isalive.start(900, now=False)
self.task_keepalive.start(3500, now=False)
self.task_prune.start(1800, now=False)
self.task_channellist.start(3600 * 24, now=False)

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,60 @@
"""
ANSI parser - this adds colour to text according to
special markup strings.
This is a IMC2 complacent version.
"""
import re
from src.utils import ansi
class IMCANSIParser(ansi.ANSIParser):
"""
This parser is per the IMC2 specification.
"""
def __init__(self):
normal = ansi.ANSI_NORMAL
hilite = ansi.ANSI_HILITE
self.ansi_map = [
(r'~Z', normal), # Random
(r'~x', normal + ansi.ANSI_BLACK), # Black
(r'~D', hilite + ansi.ANSI_BLACK), # Dark Grey
(r'~z', hilite + ansi.ANSI_BLACK),
(r'~w', normal + ansi.ANSI_WHITE), # Grey
(r'~W', hilite + ansi.ANSI_WHITE), # White
(r'~g', normal + ansi.ANSI_GREEN), # Dark Green
(r'~G', hilite + ansi.ANSI_GREEN), # Green
(r'~p', normal + ansi.ANSI_MAGENTA), # Dark magenta
(r'~m', normal + ansi.ANSI_MAGENTA),
(r'~M', hilite + ansi.ANSI_MAGENTA), # Magenta
(r'~P', hilite + ansi.ANSI_MAGENTA),
(r'~c', normal + ansi.ANSI_CYAN), # Cyan
(r'~y', normal + ansi.ANSI_YELLOW), # Dark Yellow (brown)
(r'~Y', hilite + ansi.ANSI_YELLOW), # Yellow
(r'~b', normal + ansi.ANSI_BLUE), # Dark Blue
(r'~B', hilite + ansi.ANSI_BLUE), # Blue
(r'~C', hilite + ansi.ANSI_BLUE),
(r'~r', normal + ansi.ANSI_RED), # Dark Red
(r'~R', hilite + ansi.ANSI_RED), # Red
## Formatting
(r'~L', hilite), # Bold/hilite
(r'~!', normal), # reset
(r'\\r', normal),
(r'\\n', ansi.ANSI_RETURN),
]
# prepare regex matching
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
for sub in self.ansi_map]
# prepare matching ansi codes overall
self.ansi_regex = re.compile("\033\[[0-9;]+m")
ANSI_PARSER = IMCANSIParser()
def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER):
"""
Shortcut to use the IMC2 ANSI parser.
"""
return parser.parse_ansi(string, strip_ansi=strip_ansi)

View file

@ -0,0 +1,795 @@
"""
IMC2 packets. These are pretty well documented at:
http://www.mudbytes.net/index.php?a=articles&s=imc2_protocol
"""
import shlex
from django.conf import settings
class Lexxer(shlex.shlex):
"""
A lexical parser for interpreting IMC2 packets.
"""
def __init__(self, packet_str, posix=True):
shlex.shlex.__init__(self, packet_str, posix=True)
# Single-quotes are notably not present. This is important!
self.quotes = '"'
self.commenters = ''
# This helps denote what constitutes a continuous token.
self.wordchars += "~`!@#$%^&*()-_+=[{]}|\\;:',<.>/?"
class IMC2Packet(object):
"""
Base IMC2 packet class. This is generally sub-classed, aside from using it
to parse incoming packets from the IMC2 network server.
"""
def __init__(self, mudname=None, packet_str=None):
"""
Optionally, parse a packet and load it up.
"""
# The following fields are all according to the basic packet format of:
# <sender>@<origin> <sequence> <route> <packet-type> <target>@<destination> <data...>
self.sender = None
if not mudname:
mudname = settings.SERVERNAME
self.origin = mudname
self.sequence = None
self.route = mudname
self.packet_type = None
self.target = None
self.destination = None
# Optional data.
self.optional_data = {}
# Reference to the IMC2Protocol object doing the sending.
self.imc2_protocol = None
if packet_str:
# The lexxer handles the double quotes correctly, unlike just
# splitting. Spaces throw things off, so shlex handles it
# gracefully, ala POSIX shell-style parsing.
lex = Lexxer(packet_str)
# Token counter.
counter = 0
for token in lex:
if counter == 0:
# This is the sender@origin token.
sender_origin = token
split_sender_origin = sender_origin.split('@')
self.sender = split_sender_origin[0].strip()
self.origin = split_sender_origin[1]
elif counter == 1:
# Numeric time-based sequence.
self.sequence = token
elif counter == 2:
# Packet routing info.
self.route = token
elif counter == 3:
# Packet type string.
self.packet_type = token
elif counter == 4:
# Get values for the target and destination attributes.
target_destination = token
split_target_destination = target_destination.split('@')
self.target = split_target_destination[0]
try:
self.destination = split_target_destination[1]
except IndexError:
# There is only one element to the target@dest segment
# of the packet. Wipe the target and move the captured
# value to the destination attrib.
self.target = '*'
self.destination = split_target_destination[0]
elif counter > 4:
# Populate optional data.
try:
key, value = token.split('=', 1)
self.optional_data[key] = value
except ValueError:
# Failed to split on equal sign, disregard.
pass
# Increment and continue to the next token (if applicable)
counter += 1
def __str__(self):
retval = """
--IMC2 package (%s)
Sender: %s
Origin: %s
Sequence: %s
Route: %s
Type: %s
Target: %s
Dest.: %s
Data:
%s
------------------------""" % (self.packet_type, self.sender,
self.origin, self.sequence,
self.route, self.packet_type,
self.target, self.destination,
"\n ".join(["%s: %s" % items for items in self.optional_data.items()]))
return retval.strip()
def _get_optional_data_string(self):
"""
Generates the optional data string to tack on to the end of the packet.
"""
if self.optional_data:
data_string = ''
for key, value in self.optional_data.items():
# Determine the number of words in this value.
words = len(str(value).split(' '))
# Anything over 1 word needs double quotes.
if words > 1:
value = '"%s"' % (value,)
data_string += '%s=%s ' % (key, value)
return data_string.strip()
else:
return ''
def _get_sender_name(self):
"""
Calculates the sender name to be sent with the packet.
"""
if self.sender == '*':
# Some packets have no sender.
return '*'
elif str(self.sender).isdigit():
return self.sender
elif type(self.sender) in [type(u""),type(str())]:
#this is used by e.g. IRC where no user object is present.
return self.sender.strip().replace(' ', '_')
elif self.sender:
# Player object.
name = self.sender.get_name(fullname=False, show_dbref=False,
show_flags=False,
no_ansi=True)
# IMC2 does not allow for spaces.
return name.strip().replace(' ', '_')
else:
# None value. Do something or other.
return 'Unknown'
def assemble(self, mudname=None, client_pwd=None, server_pwd=None):
"""
Assembles the packet and returns the ready-to-send string.
Note that the arguments are not used, they are there for
consistency across all packets.
"""
self.sequence = self.imc2_protocol.sequence
packet = "%s@%s %s %s %s %s@%s %s\n" % (
self._get_sender_name(),
self.origin,
self.sequence,
self.route,
self.packet_type,
self.target,
self.destination,
self._get_optional_data_string())
return packet.strip()
class IMC2PacketAuthPlaintext(object):
"""
IMC2 plain-text authentication packet. Auth packets are strangely
formatted, so this does not sub-class IMC2Packet. The SHA and plain text
auth packets are the two only non-conformers.
CLIENT Sends:
PW <mudname> <clientpw> version=<version#> autosetup <serverpw> (SHA256)
Optional Arguments( required if using the specified authentication method:
(SHA256) The literal string: SHA256. This is sent to notify the server
that the MUD is SHA256-Enabled. All future logins from this
client will be expected in SHA256-AUTH format if the server
supports it.
"""
def assemble(self, mudname=None, client_pwd=None, server_pwd=None):
"""
This is one of two strange packets, just assemble the packet manually
and go.
"""
return 'PW %s %s version=2 autosetup %s\n' %(mudname, client_pwd, server_pwd)
class IMC2PacketKeepAliveRequest(IMC2Packet):
"""
Description:
This packet is sent by a MUD to trigger is-alive packets from other MUDs.
This packet is usually followed by the sending MUD's own is-alive packet.
It is used in the filling of a client's MUD list, thus any MUD that doesn't
respond with an is-alive isn't marked as online on the sending MUD's
mudlist.
Data:
(none)
Example of a received keepalive-request:
*@YourMUD 1234567890 YourMUD!Hub1 keepalive-request *@*
Example of a sent keepalive-request:
*@YourMUD 1234567890 YourMUD keepalive-request *@*
"""
def __init__(self):
super(IMC2PacketKeepAliveRequest, self).__init__()
self.sender = '*'
self.packet_type = 'keepalive-request'
self.target = '*'
self.destination = '*'
class IMC2PacketIsAlive(IMC2Packet):
"""
Description:
This packet is the reply to a keepalive-request packet. It is responsible
for filling a client's mudlist with the information about other MUDs on the
network.
Data:
versionid=<string>
Where <string> is the text version ID of the client. ("IMC2 4.5 MUD-Net")
url=<string>
Where <string> is the proper URL of the client. (http://www.domain.com)
host=<string>
Where <string> is the telnet address of the MUD. (telnet://domain.com)
port=<int>
Where <int> is the telnet port of the MUD.
(These data fields are not sent by the MUD, they are added by the server.)
networkname=<string>
Where <string> is the network name that the MUD/server is on. ("MyNetwork")
sha256=<int>
This is an optional tag that denotes the SHA-256 capabilities of a
MUD or server.
Example of a received is-alive:
*@SomeMUD 1234567890 SomeMUD!Hub2 is-alive *@YourMUD versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" networkname="MyNetwork" sha256=1 host=domain.com port=5500
Example of a sent is-alive:
*@YourMUD 1234567890 YourMUD is-alive *@* versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" host=domain.com port=5500
"""
def __init__(self):
super(IMC2PacketIsAlive, self).__init__()
self.sender = '*'
self.packet_type = 'is-alive'
self.target = '*'
self.destination = '*'
self.optional_data = {'versionid': 'Evennia IMC2',
'url': '"http://www.evennia.com"',
'host': 'test.com',
'port': '5555'}
class IMC2PacketIceRefresh(IMC2Packet):
"""
Description:
This packet is sent by the MUD to request data about the channels on the
network. Servers with channels reply with an ice-update packet for each
channel they control. The usual target for this packet is IMC@$.
Data:
(none)
Example:
*@YourMUD 1234567890 YourMUD!Hub1 ice-refresh IMC@$
"""
def __init__(self):
super(IMC2PacketIceRefresh, self).__init__()
self.sender = '*'
self.packet_type = 'ice-refresh'
self.target = 'IMC'
self.destination = '$'
class IMC2PacketIceUpdate(IMC2Packet):
"""
Description:
A server returns this packet with the data of a channel when prompted with
an ice-refresh request.
Data:
channel=<string>
The channel's network name in the format of ServerName:ChannelName
owner=<string>
The Name@MUD of the channel's owner
operators=<string>
A space-seperated list of the Channel's operators, (format: Person@MUD)
policy=<string>
The policy is either "open" or "private" with no quotes.
invited=<string>
The space-seperated list of invited User@MUDs, only valid for a
"private" channel.
excluded=<string>
The space-seperated list of banned User@MUDs, only valid for "open"
channels.
level=<string> The default level of the channel: Admin, Imp, Imm,
Mort, or None
localname=<string> The suggested local name of the channel.
Examples:
Open Policy:
ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:ichat owner=Imm@SomeMUD operators=Other@SomeMUD policy=open excluded="Flamer@badMUD Jerk@dirtyMUD" level=Imm localname=ichat
Private Policy:
ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:secretchat owner=Imm@SomeMUD operators=Other@SomeMUD policy=private invited="SpecialDude@OtherMUD CoolDude@WeirdMUD" level=Mort localname=schat
"""
pass
class IMC2PacketIceMsgRelayed(IMC2Packet):
"""
Description:
The -r in this ice-msg packet means it was relayed. This, along with the
ice-msg-p packet, are used with private policy channels. The 'r' stands
for 'relay'. All incoming channel messages are from ICE@<server>, where
<server> is the server hosting the channel.
Data:
realfrom=<string>
The User@MUD the message came from.
channel=<string>
The Server:Channel the message is intended to be displayed on.
text=<string>
The message text.
emote=<int>
An integer value designating emotes. 0 for no emote, 1 for an emote,
and 2 for a social.
Examples:
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="Aha! I got it!" emote=0
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text=Ahh emote=0
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="grins evilly." emote=1
ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="You@YourMUD grins evilly!" emote=2
"""
pass
class IMC2PacketIceMsgPrivate(IMC2Packet):
"""
Description:
This packet is sent when a player sends a message to a private channel.
This packet should never be seen as incoming to a client. The target of
this packet should be IMC@<server> of the server hosting the channel.
Data:
channel=<string>
The Server:Channel the message is intended to be displayed on.
text=<string>
The message text.
emote=<int>
An integer value designating emotes. 0 for no emote, 1 for an emote,
and 2 for a social.
echo=<int>
Tells the server to echo the message back to the sending MUD. This is only
seen on out-going messages.
Examples:
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="Ahh! I got it!" emote=0 echo=1
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text=Ahh! emote=0 echo=1
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="grins evilly." emote=1 echo=1
You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="You@YourMUD grins evilly." emote=2 echo=1
"""
pass
class IMC2PacketIceMsgBroadcasted(IMC2Packet):
"""
Description:
This is the packet used to chat on open policy channels. When sent from a
MUD, it is broadcasted across the network. Other MUDs receive it in-tact
as it was sent by the originating MUD. The server that hosts the channel
sends the packet back to the originating MUD as an 'echo' by removing the
"echo=1" and attaching the "sender=Person@MUD" data field.
Data:
channel=<string>
The Server:Channel the message is intended to be displayed on.
text=<string>
The message text.
emote=<int>
An integer value designating emotes. 0 for no emote, 1 for an emote,
and 2 for a social.
*echo=<int>
This stays on broadcasted messages. It tells the channel's server to
relay an echo back.
*sender=<string>
The hosting server replaces "echo=1" with this when sending the echo back
to the originating MUD.
Examples:
(See above for emote/social examples as they are pretty much the same)
Return Echo Packet:
You-YourMUD@Hub1 1234567890 Hub1 ice-msg-b *@YourMUD text=Hi! channel=Hub1:ichat sender=You@YourMUD emote=0
Broadcasted Packet:
You@YourMUD 1234567890 YourMUD!Hub1 ice-msg-b *@* channel=Hub1:ichat text=Hi! emote=0 echo=1
"""
def __init__(self, server, channel, pobject, message):
"""
Args:
server: (String) Server name the channel resides on (obs - this is
e.g. Server01, not the full network name!)
channel: (String) Name of the IMC2 channel.
pobject: (Object) Object sending the message.
message: (String) Message to send.
"""
super(IMC2PacketIceMsgBroadcasted, self).__init__()
self.sender = pobject
self.packet_type = 'ice-msg-b'
self.target = '*'
self.destination = '*'
self.optional_data = {'channel': '%s:%s' % (server, channel),
'text': message,
'emote': 0,
'echo': 1}
class IMC2PacketUserCache(IMC2Packet):
"""
Description:
Sent by a MUD with a new IMC2-able player or when a player's gender changes,
this packet contains only the gender for data. The packet's origination
should be the Player@MUD.
Data:
gender=<int> 0 is male, 1 is female, 2 is anything else such as neuter.
Will be referred to as "it".
Example:
Dude@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache *@* gender=0
"""
pass
class IMC2PacketUserCacheRequest(IMC2Packet):
"""
Description:
The MUD sends this packet out when making a request for the user-cache
information of the user included in the data part of the packet.
Data:
user=<string> The Person@MUD whose data the MUD is seeking.
Example:
*@YourMUD 1234567890 YourMUD user-cache-request *@SomeMUD user=Dude@SomeMUD
"""
pass
class IMC2PacketUserCacheReply(IMC2Packet):
"""
Description:
A reply to the user-cache-request packet. It contains the user and gender
for the user.
Data:
user=<string>
The Person@MUD whose data the MUD requested.
gender=<int>
The gender of the Person@MUD in the 'user' field.
Example:
*@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache-reply *@YourMUD user=Dude@SomeMUD gender=0
"""
pass
class IMC2PacketTell(IMC2Packet):
"""
Description:
This packet is used to communicate private messages between users on MUDs
across the network.
Data:
text=<string> Message text
isreply=<int> Two settings: 1 denotes a reply, 2 denotes a tell social.
Example:
Originating:
You@YourMUD 1234567890 YourMUD tell Dude@SomeMUD text="Having fun?"
Reply from Dude:
Dude@SomeMUD 1234567890 SomeMUD!Hub1 tell You@YourMUD text="Yeah, this is cool!" isreply=1
"""
def __init__(self, pobject, target, destination, message):
super(IMC2PacketTell, self).__init__()
self.sender = pobject
self.packet_type = "tell"
self.target = target
self.destination = destination
self.optional_data = {"text": message,
"isreply":None}
def assemble(self, mudname=None, client_pwd=None, server_pwd=None):
self.sequence = self.imc2_protocol.sequence
#self.route = "%s!%s" % (self.origin, self.imc2_protocol.factory.servername.capitalize())
return '''"%s@%s %s %s tell %s@%s text="%s"''' % (self.sender, self.origin, self.sequence,
self.route, self.target, self.destination,
self.optional_data.get("text","NO TEXT GIVEN"))
class IMC2PacketEmote(IMC2Packet):
"""
Description:
This packet seems to be sent by servers when notifying the network of a new
channel or the destruction of a channel.
Data:
channel=<int>
Unsure of what this means. The channel seen in both creation and
destruction packets is 15.
level=<int>
I am assuming this is the permission level of the sender. In both
creation and destruction messages, this is -1.
text=<string>
This is the message to be sent to the users.
Examples:
ICE@Hub1 1234567890 Hub1 emote *@* channel=15 level=-1 text="the
channel called hub1:test has been destroyed by You@YourMUD."
"""
pass
class IMC2PacketRemoteAdmin(IMC2Packet):
"""
Description:
This packet is used in remote server administration. Please note that
SHA-256 Support is *required* for a client to use this feature. The command
can vary, in fact this very packet is highly dependant on the server it's
being directed to. In most cases, sending the 'list' command will have a
remote-admin enabled server send you the list of commands it will accept.
Data:
command=<string>
The command being sent to the server for processing.
data=<string>
Data associated with the command. This is not always required.
hash=<string>
The SHA-256 hash that is verified by the server. This hash is generated in
the same manner as an authentication packet.
Example:
You@YourMUD 1234567890 YourMUD remote-admin IMC@Hub1 command=list hash=<hash goes here>
"""
pass
class IMC2PacketIceCmd(IMC2Packet):
"""
Description:
Used for remote channel administration. In most cases, one must be listed
as a channel creator on the target server in order to do much with this
packet. Other cases include channel operators.
Data:
channel=<string>
The target server:channel for the command.
command=<string>
The command to be processed.
data=<string>
Data associated with the command. This is not always required.
Example:
You@YourMUD 1234567890 YourMUD ice-cmd IMC@hub1 channel=hub1:ichat command=list
"""
pass
class IMC2PacketDestroy(IMC2Packet):
"""
Description:
Sent by a server to indicate the destruction of a channel it hosted.
The mud should remove this channel from its local configuration.
Data:
channel=<string> The server:channel being destroyed.
"""
pass
class IMC2PacketWho(IMC2Packet):
"""
Description:
A seemingly mutli-purpose information-requesting packet. The istats
packet currently only works on servers, or at least that's the case on
MUD-Net servers. The 'finger' type takes a player name in addition to the
type name.
Example: "finger Dude". The 'who' and 'info' types take no argument.
The MUD is responsible for building the reply text sent in the who-reply
packet.
Data:
type=<string> Types: who, info, "finger <name>", istats (server only)
Example:
Dude@SomeMUD 1234567890 SomeMUD!Hub1 who *@YourMUD type=who
"""
pass
class IMC2PacketWhoReply(IMC2Packet):
"""
Description:
The multi-purpose reply to the multi-purpose information-requesting 'who'
packet. The MUD is responsible for building the return data, including the
format of it. The mud can use the permission level sent in the original who
packet to filter the output. The example below is the MUD-Net format.
Data:
text=<string> The formatted reply to a 'who' packet.
Additional Notes:
The example below is for the who list packet. The same construction would
go into formatting the other types of who packets.
Example:
*@YourMUD 1234567890 YourMUD who-reply Dude@SomeMUD text="\n\r~R-=< ~WPlayers on YourMUD ~R>=-\n\r ~Y-=< ~Wtelnet://yourmud.domain.com:1234 ~Y>=-\n\r\n\r~B--------------------------------=< ~WPlayers ~B>=---------------------------------\n\r\n\r ~BPlayer ~z<--->~G Mortal the Toy\n\r\n\r~R-------------------------------=< ~WImmortals ~R>=--------------------------------\n\r\n\r ~YStaff ~z<--->~G You the Immortal\n\r\n\r~Y<~W2 Players~Y> ~Y<~WHomepage: http://www.yourmud.com~Y> <~W 2 Max Since Reboot~Y>\n\r~Y<~W3 logins since last reboot on Tue Feb 24, 2004 6:55:59 PM EST~Y>"
"""
pass
class IMC2PacketWhois(IMC2Packet):
"""
Description:
Sends a request to the network for the location of the specified player.
Data:
level=<int> The permission level of the person making the request.
Example:
You@YourMUD 1234567890 YourMUD whois dude@* level=5
"""
def __init__(self, pobject_id, whois_target):
super(IMC2PacketWhois, self).__init__()
# Use the dbref, it's easier to trace back for the whois-reply.
self.sender = pobject_id
self.packet_type = 'whois'
self.target = whois_target
self.destination = '*'
self.optional_data = {'level': '5'}
class IMC2PacketWhoisReply(IMC2Packet):
"""
Description:
The reply to a whois packet. The MUD is responsible for building and formatting
the text sent back to the requesting player, and can use the permission level
sent in the original whois packet to filter or block the response.
Data:
text=<string> The whois text.
Example:
*@SomeMUD 1234567890 SomeMUD!Hub1 whois-reply You@YourMUD text="~RIMC Locate: ~YDude@SomeMUD: ~cOnline.\n\r"
"""
pass
class IMC2PacketBeep(IMC2Packet):
"""
Description:
Sends out a beep packet to the Player@MUD. The client receiving this should
then send a bell-character to the target player to 'beep' them.
Example:
You@YourMUD 1234567890 YourMUD beep dude@somemud
"""
pass
class IMC2PacketIceChanWho(IMC2Packet):
"""
Description:
Sends a request to the specified MUD or * to list all the users listening
to the specified channel.
Data:
level=<int>
Sender's permission level.
channel=<string>
The server:chan name of the channel.
lname=<string>
The localname of the channel.
Example:
You@YourMUD 1234567890 YourMUD ice-chan-who somemud level=5 channel=Hub1:ichat lname=ichat
"""
pass
class IMC2PacketIceChanWhoReply(IMC2Packet):
"""
Description:
This is the reply packet for an ice-chan-who. The MUD is responsible for
creating and formatting the list sent back in the 'list' field. The
permission level sent in the original ice-chan-who packet can be used to
filter or block the response.
Data:
channel=<string>
The server:chan of the requested channel.
list=<string>
The formatted list of local listeners for that MUD.
Example:
*@SomeMUD 1234567890 SomeMUD!Hub1 ice-chan-whoreply You@YourMUD channel=Hub1:ichat list="The following people are listening to ichat on SomeMUD:\n\r\n\rDude\n\r"
"""
pass
class IMC2PacketLaston(IMC2Packet):
"""
Description:
This packet queries the server the mud is connected to to find out when a
specified user was last seen by the network on a public channel.
Data:
username=<string> The user, user@mud, or "all" being queried. Responses
to this packet will be sent by the server in the form of a series of tells.
Example: User@MUD 1234567890 MUD imc-laston SERVER username=somenamehere
"""
pass
class IMC2PacketCloseNotify(IMC2Packet):
"""
Description:
This packet alerts the network when a server or MUD has disconnected. The
server hosting the server or MUD is responsible for sending this packet
out across the network. Clients need only process the packet to remove the
disconnected MUD from their MUD list (or mark it as Disconnected).
Data:
host=<string>
The MUD or server that has disconnected from the network.
Example:
*@Hub2 1234567890 Hub2!Hub1 close-notify *@* host=DisconnMUD
"""
pass
if __name__ == "__main__":
packstr = "Kayle@MW 1234567 MW!Server02!Server01 ice-msg-b *@* channel=Server01:ichat text=\"*they're going woot\" emote=0 echo=1"
packstr = "*@Lythelian 1234567 Lythelian!Server01 is-alive *@* versionid=\"Tim's LPC IMC2 client 30-Jan-05 / Dead Souls integrated\" networkname=Mudbytes url=http://dead-souls.net host=70.32.76.142 port=6666 sha256=0"
print IMC2Packet(packstr)

125
lib/server/portal/irc.py Normal file
View file

@ -0,0 +1,125 @@
"""
This connects to an IRC network/channel and launches an 'bot' onto it.
The bot then pipes what is being said between the IRC channel and one or
more Evennia channels.
"""
from twisted.application import internet
from twisted.words.protocols import irc
from twisted.internet import protocol
from src.server.session import Session
from src.utils import logger
# IRC bot
class IRCBot(irc.IRCClient, Session):
"""
An IRC bot that tracks actitivity in a channel as well
as sends text to it when prompted
"""
lineRate = 1
# assigned by factory at creation
nickname = None
logger = None
factory = None
channel = None
def signedOn(self):
"""
This is called when we successfully connect to
the network. We make sure to now register with
the game as a full session.
"""
self.join(self.channel)
self.stopping = False
self.factory.bot = self
address = "%s@%s" % (self.channel, self.network)
self.init_session("ircbot", address, self.factory.sessionhandler)
# we link back to our bot and log in
self.uid = int(self.factory.uid)
self.logged_in = True
self.factory.sessionhandler.connect(self)
logger.log_infomsg("IRC bot '%s' connected to %s at %s:%s." % (self.nickname, self.channel,
self.network, self.port))
def disconnect(self, reason=None):
"""
Called by sessionhandler to disconnect this protocol
"""
print "irc disconnect called!"
self.sessionhandler.disconnect(self)
self.stopping = True
self.transport.loseConnection()
def privmsg(self, user, channel, msg):
"A message was sent to channel"
if not msg.startswith('***'):
user = user.split('!', 1)[0]
self.data_in("bot_data_in %s@%s: %s" % (user, channel, msg))
def action(self, user, channel, msg):
"An action was done in channel"
if not msg.startswith('**'):
user = user.split('!', 1)[0]
self.data_in("bot_data_in %s@%s %s" % (user, channel, msg))
def data_in(self, text=None, **kwargs):
"Data IRC -> Server"
self.sessionhandler.data_in(self, text=text, **kwargs)
def data_out(self, text=None, **kwargs):
"Data from server-> IRC"
if text.startswith("bot_data_out"):
text = text.split(" ", 1)[1]
self.say(self.channel, text)
class IRCBotFactory(protocol.ReconnectingClientFactory):
"""
Creates instances of AnnounceBot, connecting with
a staggered increase in delay
"""
# scaling reconnect time
initialDelay = 1
factor = 1.5
maxDelay = 60
def __init__(self, sessionhandler, uid=None, botname=None, channel=None, network=None, port=None):
"Storing some important protocol properties"
self.sessionhandler = sessionhandler
self.uid = uid
self.nickname = str(botname)
self.channel = str(channel)
self.network = str(network)
self.port = port
self.bot = None
def buildProtocol(self, addr):
"Build the protocol and assign it some properties"
protocol = IRCBot()
protocol.factory = self
protocol.nickname = self.nickname
protocol.channel = self.channel
protocol.network = self.network
protocol.port = self.port
return protocol
def startedConnecting(self, connector):
"Tracks reconnections for debugging"
logger.log_infomsg("(re)connecting to %s" % self.channel)
def clientConnectionFailed(self, connector, reason):
self.retry(connector)
def clientConnectionLost(self, connector, reason):
if not self.bot.stopping:
self.retry(connector)
def start(self):
"Connect session to sessionhandler"
if self.port:
service = internet.TCPClient(self.network, int(self.port), self)
self.sessionhandler.portal.services.addService(service)

67
lib/server/portal/mccp.py Normal file
View file

@ -0,0 +1,67 @@
"""
MCCP - Mud Client Compression Protocol
This implements the MCCP v2 telnet protocol as per
http://tintin.sourceforge.net/mccp/. MCCP allows for the server to
compress data when sending to supporting clients, reducing bandwidth
by 70-90%.. The compression is done using Python's builtin zlib
library. If the client doesn't support MCCP, server sends uncompressed
as normal. Note: On modern hardware you are not likely to notice the
effect of MCCP unless you have extremely heavy traffic or sits on a
terribly slow connection.
This protocol is implemented by the telnet protocol importing
mccp_compress and calling it from its write methods.
"""
import zlib
# negotiations for v1 and v2 of the protocol
MCCP = chr(86)
FLUSH = zlib.Z_SYNC_FLUSH
def mccp_compress(protocol, data):
"Handles zlib compression, if applicable"
if hasattr(protocol, 'zlib'):
return protocol.zlib.compress(data) + protocol.zlib.flush(FLUSH)
return data
class Mccp(object):
"""
Implements the MCCP protocol. Add this to a
variable on the telnet protocol to set it up.
"""
def __init__(self, protocol):
"""
initialize MCCP by storing protocol on
ourselves and calling the client to see if
it supports MCCP. Sets callbacks to
start zlib compression in that case.
"""
self.protocol = protocol
self.protocol.protocol_flags['MCCP'] = False
# ask if client will mccp, connect callbacks to handle answer
self.protocol.will(MCCP).addCallbacks(self.do_mccp, self.no_mccp)
def no_mccp(self, option):
"""
Called if client doesn't support mccp or chooses to turn it off
"""
if hasattr(self.protocol, 'zlib'):
del self.protocol.zlib
self.protocol.protocol_flags['MCCP'] = False
self.protocol.handshake_done()
def do_mccp(self, option):
"""
The client supports MCCP. Set things up by
creating a zlib compression stream.
"""
self.protocol.protocol_flags['MCCP'] = True
self.protocol.requestNegotiation(MCCP, '')
self.protocol.zlib = zlib.compressobj(9)
self.protocol.handshake_done()

243
lib/server/portal/msdp.py Normal file
View file

@ -0,0 +1,243 @@
"""
MSDP (Mud Server Data Protocol)
This implements the MSDP protocol as per
http://tintin.sourceforge.net/msdp/. MSDP manages out-of-band
communication between the client and server, for updating health bars
etc.
"""
import re
from src.utils.utils import to_str
# MSDP-relevant telnet cmd/opt-codes
MSDP = chr(69)
MSDP_VAR = chr(1)
MSDP_VAL = chr(2)
MSDP_TABLE_OPEN = chr(3)
MSDP_TABLE_CLOSE = chr(4)
MSDP_ARRAY_OPEN = chr(5)
MSDP_ARRAY_CLOSE = chr(6)
IAC = chr(255)
SB = chr(250)
SE = chr(240)
force_str = lambda inp: to_str(inp, force_string=True)
# pre-compiled regexes
# returns 2-tuple
regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_ARRAY_OPEN,
MSDP_ARRAY_CLOSE))
# returns 2-tuple (may be nested)
regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_TABLE_OPEN,
MSDP_TABLE_CLOSE))
regex_var = re.compile(MSDP_VAR)
regex_val = re.compile(MSDP_VAL)
# Msdp object handler
class Msdp(object):
"""
Implements the MSDP protocol.
"""
def __init__(self, protocol):
"""
Initiates by storing the protocol
on itself and trying to determine
if the client supports MSDP.
"""
self.protocol = protocol
self.protocol.protocol_flags['MSDP'] = False
self.protocol.negotiationMap[MSDP] = self.msdp_to_evennia
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
self.msdp_reported = {}
def no_msdp(self, option):
"No msdp supported or wanted"
self.protocol.handshake_done()
def do_msdp(self, option):
"""
Called when client confirms that it can do MSDP.
"""
self.protocol.protocol_flags['MSDP'] = True
self.protocol.handshake_done()
def evennia_to_msdp(self, cmdname, *args, **kwargs):
"""
handle return data from cmdname by converting it to
a proper msdp structure. data can either be a single value (will be
converted to a string), a list (will be converted to an MSDP_ARRAY),
or a dictionary (will be converted to MSDP_TABLE).
OBS - there is no actual use of arrays and tables in the MSDP
specification or default commands -- are returns are implemented
as simple lists or named lists (our name for them here, these
un-bounded structures are not named in the specification). So for
now, this routine will not explicitly create arrays nor tables,
although there are helper methods ready should it be needed in
the future.
"""
def make_table(name, **kwargs):
"build a table that may be nested with other tables or arrays."
string = MSDP_VAR + force_str(name) + MSDP_VAL + MSDP_TABLE_OPEN
for key, val in kwargs.items():
if isinstance(val, dict):
string += make_table(string, key, **val)
elif hasattr(val, '__iter__'):
string += make_array(string, key, *val)
else:
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
string += MSDP_TABLE_CLOSE
return string
def make_array(name, *args):
"build a array. Arrays may not nest tables by definition."
string = MSDP_VAR + force_str(name) + MSDP_ARRAY_OPEN
string += MSDP_VAL.join(force_str(arg) for arg in args)
string += MSDP_ARRAY_CLOSE
return string
def make_list(name, *args):
"build a simple list - an array without start/end markers"
string = MSDP_VAR + force_str(name)
string += MSDP_VAL.join(force_str(arg) for arg in args)
return string
def make_named_list(name, **kwargs):
"build a named list - a table without start/end markers"
string = MSDP_VAR + force_str(name)
for key, val in kwargs.items():
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
return string
# Default MSDP commands
print "MSDP outgoing:", cmdname, args, kwargs
cupper = cmdname.upper()
if cupper == "LIST":
if args:
args = list(args)
mode = args.pop(0).upper()
self.data_out(make_array(mode, *args))
elif cupper == "REPORT":
self.data_out(make_list("REPORT", *args))
elif cupper == "UNREPORT":
self.data_out(make_list("UNREPORT", *args))
elif cupper == "RESET":
self.data_out(make_list("RESET", *args))
elif cupper == "SEND":
self.data_out(make_named_list("SEND", **kwargs))
else:
# return list or named lists.
msdp_string = ""
if args:
msdp_string += make_list(cupper, *args)
if kwargs:
msdp_string += make_named_list(cupper, **kwargs)
self.data_out(msdp_string)
def msdp_to_evennia(self, data):
"""
Handle a client's requested negotiation, converting
it into a function mapping - either one of the MSDP
default functions (LIST, SEND etc) or a custom one
in OOB_FUNCS dictionary. command names are case-insensitive.
varname, var --> mapped to function varname(var)
arrayname, array --> mapped to function arrayname(*array)
tablename, table --> mapped to function tablename(**table)
Note: Combinations of args/kwargs to one function is not supported
in this implementation (it complicates the code for limited
gain - arrayname(*array) is usually as complex as anyone should
ever need to go anyway (I hope!).
"""
tables = {}
arrays = {}
variables = {}
if hasattr(data, "__iter__"):
data = "".join(data)
#logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
for key, table in regex_table.findall(data):
tables[key] = {}
for varval in regex_var.split(table):
parts = regex_val.split(varval)
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)})
for key, array in regex_array.findall(data):
arrays[key] = []
for val in regex_val.split(array):
arrays[key].append(val)
arrays[key] = tuple(arrays[key])
for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))):
# get remaining varvals after cleaning away tables/arrays
parts = regex_val.split(varval)
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", )
#print "MSDP: table, array, variables:", tables, arrays, variables
# all variables sent through msdp to Evennia are considered commands
# with arguments. There are three forms of commands possible
# through msdp:
#
# VARNAME VAR -> varname(var)
# ARRAYNAME VAR VAL VAR VAL VAR VAL ENDARRAY -> arrayname(val,val,val)
# TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE ->
# tablename(varname=val, varname=val)
#
# default MSDP functions
if "LIST" in variables:
self.data_in("list", *variables.pop("LIST"))
if "REPORT" in variables:
self.data_in("report", *variables.pop("REPORT"))
if "REPORT" in arrays:
self.data_in("report", *(arrays.pop("REPORT")))
if "UNREPORT" in variables:
self.data_in("unreport", *(arrays.pop("UNREPORT")))
if "RESET" in variables:
self.data_in("reset", *variables.pop("RESET"))
if "RESET" in arrays:
self.data_in("reset", *(arrays.pop("RESET")))
if "SEND" in variables:
self.data_in("send", *variables.pop("SEND"))
if "SEND" in arrays:
self.data_in("send", *(arrays.pop("SEND")))
# if there are anything left consider it a call to a custom function
for varname, var in variables.items():
# a simple function + argument
self.data_in(varname, (var,))
for arrayname, array in arrays.items():
# we assume the array are multiple arguments to the function
self.data_in(arrayname, *array)
for tablename, table in tables.items():
# we assume tables are keyword arguments to the function
self.data_in(tablename, **table)
def data_out(self, msdp_string):
"""
Return a msdp-valid subnegotiation across the protocol.
"""
#print "msdp data_out (without IAC SE):", msdp_string
self.protocol ._write(IAC + SB + MSDP + force_str(msdp_string) + IAC + SE)
def data_in(self, funcname, *args, **kwargs):
"""
Send oob data to Evennia
"""
#print "msdp data_in:", funcname, args, kwargs
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))

185
lib/server/portal/mssp.py Normal file
View file

@ -0,0 +1,185 @@
"""
MSSP - Mud Server Status Protocol
This implements the MSSP telnet protocol as per
http://tintin.sourceforge.net/mssp/. MSSP allows web portals and
listings to have their crawlers find the mud and automatically
extract relevant information about it, such as genre, how many
active players and so on.
"""
from django.conf import settings
from src.utils import utils
MSSP = chr(70)
MSSP_VAR = chr(1)
MSSP_VAL = chr(2)
# try to get the customized mssp info, if it exists.
MSSPTable_CUSTOM = utils.variable_from_module(settings.MSSP_META_MODULE, "MSSPTable", default={})
class Mssp(object):
"""
Implements the MSSP protocol. Add this to a
variable on the telnet protocol to set it up.
"""
def __init__(self, protocol):
"""
initialize MSSP by storing protocol on ourselves
and calling the client to see if it supports
MSSP.
"""
self.protocol = protocol
self.protocol.will(MSSP).addCallbacks(self.do_mssp, self.no_mssp)
def get_player_count(self):
"Get number of logged-in players"
return str(self.protocol.sessionhandler.count_loggedin())
def get_uptime(self):
"Get how long the portal has been online (reloads are not counted)"
return str(self.protocol.sessionhandler.uptime)
def no_mssp(self, option):
"""
This is the normal operation.
"""
self.protocol.handshake_done()
pass
def do_mssp(self, option):
"""
Negotiate all the information.
"""
self.mssp_table = {
# Required fields
"NAME": "Evennia",
"PLAYERS": self.get_player_count,
"UPTIME" : self.get_uptime,
# Generic
"CRAWL DELAY": "-1",
"HOSTNAME": "", # current or new hostname
"PORT": ["4000"], # most important port should be last in list
"CODEBASE": "Evennia",
"CONTACT": "", # email for contacting the mud
"CREATED": "", # year MUD was created
"ICON": "", # url to icon 32x32 or larger; <32kb.
"IP": "", # current or new IP address
"LANGUAGE": "", # name of language used, e.g. English
"LOCATION": "", # full English name of server country
"MINIMUM AGE": "0", # set to 0 if not applicable
"WEBSITE": "www.evennia.com",
# Categorisation
"FAMILY": "Custom", # evennia goes under 'Custom'
"GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
"GAMEPLAY": "None", # Adventure, Educational, Hack and Slash, None,
# Player versus Player, Player versus Environment,
# Roleplaying, Simulation, Social or Strategy
"STATUS": "Open Beta", # Alpha, Closed Beta, Open Beta, Live
"GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew
"INTERMUD": "IMC2", # evennia supports IMC2.
"SUBGENRE": "None", # LASG, Medieval Fantasy, World War II, Frankenstein,
# Cyberpunk, Dragonlance, etc. Or None if not available.
# World
"AREAS": "0",
"HELPFILES": "0",
"MOBILES": "0",
"OBJECTS": "0",
"ROOMS": "0", # use 0 if room-less
"CLASSES": "0", # use 0 if class-less
"LEVELS": "0", # use 0 if level-less
"RACES": "0", # use 0 if race-less
"SKILLS": "0", # use 0 if skill-less
# Protocols set to 1 or 0)
"ANSI": "1",
"GMCP": "0",
"MCCP": "0",
"MCP": "0",
"MSDP": "0",
"MSP": "0",
"MXP": "0",
"PUEBLO": "0",
"UTF-8": "1",
"VT100": "0",
"XTERM 256 COLORS": "0",
# Commercial set to 1 or 0)
"PAY TO PLAY": "0",
"PAY FOR PERKS": "0",
# Hiring set to 1 or 0)
"HIRING BUILDERS": "0",
"HIRING CODERS": "0",
# Extended variables
# World
"DBSIZE": "0",
"EXITS": "0",
"EXTRA DESCRIPTIONS": "0",
"MUDPROGS": "0",
"MUDTRIGS": "0",
"RESETS": "0",
# Game (set to 1, 0 or one of the given alternatives)
"ADULT MATERIAL": "0",
"MULTICLASSING": "0",
"NEWBIE FRIENDLY": "0",
"PLAYER CITIES": "0",
"PLAYER CLANS": "0",
"PLAYER CRAFTING": "0",
"PLAYER GUILDS": "0",
"EQUIPMENT SYSTEM": "None", # "None", "Level", "Skill", "Both"
"MULTIPLAYING": "None", # "None", "Restricted", "Full"
"PLAYERKILLING": "None", # "None", "Restricted", "Full"
"QUEST SYSTEM": "None", # "None", "Immortal Run", "Automated", "Integrated"
"ROLEPLAYING": "None", # "None", "Accepted", "Encouraged", "Enforced"
"TRAINING SYSTEM": "None", # "None", "Level", "Skill", "Both"
"WORLD ORIGINALITY": "None", # "All Stock", "Mostly Stock", "Mostly Original", "All Original"
# Protocols (only change if you added/removed something manually)
"ATCP": "0",
"MSDP": "0",
"MCCP": "1",
"SSL": "1",
"UTF-8": "1",
"ZMP": "0",
"XTERM 256 COLORS": "0"}
# update the static table with the custom one
if MSSPTable_CUSTOM:
self.mssp_table.update(MSSPTable_CUSTOM)
varlist = ''
for variable, value in self.mssp_table.items():
if callable(value):
value = value()
if utils.is_iter(value):
for partval in value:
varlist += MSSP_VAR + str(variable) + MSSP_VAL + str(partval)
else:
varlist += MSSP_VAR + str(variable) + MSSP_VAL + str(value)
# send to crawler by subnegotiation
self.protocol.requestNegotiation(MSSP, varlist)
self.protocol.handshake_done()

62
lib/server/portal/mxp.py Normal file
View file

@ -0,0 +1,62 @@
"""
MXP - Mud eXtension Protocol.
Partial implementation of the MXP protocol.
The MXP protocol allows more advanced formatting options for telnet clients
that supports it (mudlet, zmud, mushclient are a few)
This only implements the SEND tag.
More information can be found on the following links:
http://www.zuggsoft.com/zmud/mxp.htm
http://www.mushclient.com/mushclient/mxp.htm
http://www.gammon.com.au/mushclient/addingservermxp.htm
"""
import re
LINKS_SUB = re.compile(r'\{lc(.*?)\{lt(.*?)\{le', re.DOTALL)
MXP = "\x5B"
MXP_TEMPSECURE = "\x1B[4z"
MXP_SEND = MXP_TEMPSECURE + \
"<SEND HREF='\\1'>" + \
"\\2" + \
MXP_TEMPSECURE + \
"</SEND>"
def mxp_parse(text):
"""
Replaces links to the correct format for MXP.
"""
text = text.replace("&", "&amp;") \
.replace("<", "&lt;") \
.replace(">", "&gt;")
text = LINKS_SUB.sub(MXP_SEND, text)
return text
class Mxp(object):
"""
Implements the MXP protocol.
"""
def __init__(self, protocol):
"""Initializes the protocol by checking if the client supports it."""
self.protocol = protocol
self.protocol.protocol_flags["MXP"] = False
self.protocol.will(MXP).addCallbacks(self.do_mxp, self.no_mxp)
def no_mxp(self, option):
"""
Client does not support MXP.
"""
self.protocol.protocol_flags["MXP"] = False
self.protocol.handshake_done()
def do_mxp(self, option):
"""
Client does support MXP.
"""
self.protocol.protocol_flags["MXP"] = True
self.protocol.handshake_done()
self.protocol.requestNegotiation(MXP, '')

61
lib/server/portal/naws.py Normal file
View file

@ -0,0 +1,61 @@
"""
NAWS - Negotiate About Window Size
This implements the NAWS telnet option as per
https://www.ietf.org/rfc/rfc1073.txt
NAWS allows telnet clients to report their
current window size to the client and update
it when the size changes
"""
from django.conf import settings
from src.utils import utils
NAWS = chr(31)
IS = chr(0)
# default taken from telnet specification
DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
DEFAULT_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT
# try to get the customized mssp info, if it exists.
class Naws(object):
"""
Implements the MSSP protocol. Add this to a
variable on the telnet protocol to set it up.
"""
def __init__(self, protocol):
"""
initialize NAWS by storing protocol on ourselves
and calling the client to see if it supports
NAWS.
"""
self.naws_step = 0
self.protocol = protocol
self.protocol.protocol_flags['SCREENWIDTH'] = {0: DEFAULT_WIDTH} # windowID (0 is root):width
self.protocol.protocol_flags['SCREENHEIGHT'] = {0: DEFAULT_HEIGHT} # windowID:width
self.protocol.negotiationMap[NAWS] = self.negotiate_sizes
self.protocol.do(NAWS).addCallbacks(self.do_naws, self.no_naws)
def no_naws(self, option):
"""
This is the normal operation.
"""
self.protocol.handshake_done()
def do_naws(self, option):
"""
Negotiate all the information.
"""
self.protocol.handshake_done()
def negotiate_sizes(self, options):
if len(options) == 4:
# NAWS is negotiated with 16bit words
width = options[0] + options[1]
self.protocol.protocol_flags['SCREENWIDTH'][0] = int(width.encode('hex'), 16)
height = options[2] + options[3]
self.protocol.protocol_flags['SCREENHEIGHT'][0] = int(height.encode('hex'), 16)

312
lib/server/portal/portal.py Normal file
View file

@ -0,0 +1,312 @@
"""
This module implements the main Evennia server process, the core of
the game engine.
This module should be started with the 'twistd' executable since it
sets up all the networking features. (this is done automatically
by game/evennia.py).
"""
import sys
import os
if os.name == 'nt':
# For Windows batchfile we need an extra path insertion here.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__))))))
from src.server.webserver import EvenniaReverseProxyResource
from twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.web import server
import django
django.setup()
from django.conf import settings
from src.utils.utils import get_evennia_version, mod_import, make_iter
from src.server.portal.portalsessionhandler import PORTAL_SESSIONS
PORTAL_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)]
if os.name == 'nt':
# For Windows we need to handle pid files manually.
PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, 'portal.pid')
#------------------------------------------------------------
# Evennia Portal settings
#------------------------------------------------------------
VERSION = get_evennia_version()
SERVERNAME = settings.SERVERNAME
PORTAL_RESTART = os.path.join(settings.GAME_DIR, 'portal.restart')
TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT
TELNET_INTERFACES = settings.TELNET_INTERFACES
SSL_INTERFACES = settings.SSL_INTERFACES
SSH_INTERFACES = settings.SSH_INTERFACES
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
WEBSOCKET_CLIENT_INTERFACE = settings.WEBSOCKET_CLIENT_INTERFACE
WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
AMP_INTERFACE = settings.AMP_INTERFACE
AMP_ENABLED = AMP_HOST and AMP_PORT and AMP_INTERFACE
#------------------------------------------------------------
# Portal Service object
#------------------------------------------------------------
class Portal(object):
"""
The main Portal server handler. This object sets up the database and
tracks and interlinks all the twisted network services that make up
Portal.
"""
def __init__(self, application):
"""
Setup the server.
application - an instantiated Twisted application
"""
sys.path.append('.')
# create a store of services
self.services = service.IServiceCollection(application)
self.amp_protocol = None # set by amp factory
self.sessions = PORTAL_SESSIONS
self.sessions.portal = self
# set a callback if the server is killed abruptly,
# by Ctrl-C, reboot etc.
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True)
self.game_running = False
def set_restart_mode(self, mode=None):
"""
This manages the flag file that tells the runner if the server should
be restarted or is shutting down. Valid modes are True/False and None.
If mode is None, no change will be done to the flag file.
"""
if mode is None:
return
f = open(PORTAL_RESTART, 'w')
print "writing mode=%(mode)s to %(portal_restart)s" % {'mode': mode, 'portal_restart': PORTAL_RESTART}
f.write(str(mode))
f.close()
def shutdown(self, restart=None, _reactor_stopping=False):
"""
Shuts down the server from inside it.
restart - True/False sets the flags so the server will be
restarted or not. If None, the current flag setting
(set at initialization or previous runs) is used.
_reactor_stopping - this is set if server is already in the process of
shutting down; in this case we don't need to stop it again.
Note that restarting (regardless of the setting) will not work
if the Portal is currently running in daemon mode. In that
case it always needs to be restarted manually.
"""
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# we get here due to us calling reactor.stop below. No need
# to do the shutdown procedure again.
return
self.set_restart_mode(restart)
if os.name == 'nt' and os.path.exists(PORTAL_PIDFILE):
# for Windows we need to remove pid files manually
os.remove(PORTAL_PIDFILE)
if not _reactor_stopping:
# shutting down the reactor will trigger another signal. We set
# a flag to avoid loops.
self.shutdown_complete = True
reactor.callLater(0, reactor.stop)
#------------------------------------------------------------
#
# Start the Portal proxy server and add all active services
#
#------------------------------------------------------------
# twistd requires us to define the variable 'application' so it knows
# what to execute from.
application = service.Application('Portal')
# The main Portal server program. This sets up the database
# and is where we store all the other services.
PORTAL = Portal(application)
print '-' * 50
print ' %(servername)s Portal (%(version)s) started.' % {'servername': SERVERNAME, 'version': VERSION}
if AMP_ENABLED:
# The AMP protocol handles the communication between
# the portal and the mud server. Only reason to ever deactivate
# it would be during testing and debugging.
from src.server import amp
print ' amp (to Server): %s' % AMP_PORT
factory = amp.AmpClientFactory(PORTAL)
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
amp_client.setName('evennia_amp')
PORTAL.services.addService(amp_client)
# We group all the various services under the same twisted app.
# These will gradually be started as they are initialized below.
if TELNET_ENABLED:
# Start telnet game connections
from src.server.portal import telnet
for interface in TELNET_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(TELNET_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in TELNET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.protocol = telnet.TelnetProtocol
factory.sessionhandler = PORTAL_SESSIONS
telnet_service = internet.TCPServer(port, factory, interface=interface)
telnet_service.setName('EvenniaTelnet%s' % pstring)
PORTAL.services.addService(telnet_service)
print ' telnet%s: %s' % (ifacestr, port)
if SSL_ENABLED:
# Start SSL game connection (requires PyOpenSSL).
from src.server.portal import ssl
for interface in SSL_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(SSL_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSL_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.sessionhandler = PORTAL_SESSIONS
factory.protocol = ssl.SSLProtocol
ssl_service = internet.SSLServer(port,
factory,
ssl.getSSLContext(),
interface=interface)
ssl_service.setName('EvenniaSSL%s' % pstring)
PORTAL.services.addService(ssl_service)
print " ssl%s: %s" % (ifacestr, port)
if SSH_ENABLED:
# Start SSH game connections. Will create a keypair in
# evennia/game if necessary.
from src.server.portal import ssh
for interface in SSH_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(SSH_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory({'protocolFactory': ssh.SshProtocol,
'protocolArgs': (),
'sessions': PORTAL_SESSIONS})
ssh_service = internet.TCPServer(port, factory, interface=interface)
ssh_service.setName('EvenniaSSH%s' % pstring)
PORTAL.services.addService(ssh_service)
print " ssl%s: %s" % (ifacestr, port)
if WEBSERVER_ENABLED:
# Start a reverse proxy to relay data to the Server-side webserver
websocket_started = False
for interface in WEBSERVER_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
ifacestr = "-%s" % interface
for proxyport, serverport in WEBSERVER_PORTS:
pstring = "%s:%s<->%s" % (ifacestr, proxyport, serverport)
web_root = EvenniaReverseProxyResource('127.0.0.1', serverport, '')
webclientstr = ""
if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
from src.server.portal.webclient import WebClient
webclient = WebClient()
webclient.sessionhandler = PORTAL_SESSIONS
web_root.putChild("webclientdata", webclient)
webclientstr = "\n + client (ajax only)"
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
# start websocket client port for the webclient
# we only support one websocket client
from src.server.portal import websocket_client
from src.utils.txws import WebSocketFactory
interface = WEBSOCKET_CLIENT_INTERFACE
port = WEBSOCKET_CLIENT_PORT
ifacestr = ""
if interface not in ('0.0.0.0', '::'):
ifacestr = "-%s" % interface
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.protocol = websocket_client.WebSocketClient
factory.sessionhandler = PORTAL_SESSIONS
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
websocket_service.setName('EvenniaWebSocket%s' % pstring)
PORTAL.services.addService(websocket_service)
websocket_started = True
webclientstr = webclientstr[:-11] + "(%s:%s)" % (WEBSOCKET_CLIENT_URL, port)
web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
proxy_service = internet.TCPServer(proxyport,
web_root,
interface=interface)
proxy_service.setName('EvenniaWebProxy%s' % pstring)
PORTAL.services.addService(proxy_service)
print " webproxy%s:%s (<-> %s)%s" % (ifacestr, proxyport, serverport, webclientstr)
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start
plugin_module.start_plugin_services(PORTAL)
print '-' * 50 # end of terminal output
if os.name == 'nt':
# Windows only: Set PID file manually
f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w')
f.write(str(os.getpid()))
f.close()

View file

@ -0,0 +1,208 @@
"""
Sessionhandler for portal sessions
"""
import time
from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN, PSYNC, PCONNSYNC
_MOD_IMPORT = None
#------------------------------------------------------------
# Portal-SessionHandler class
#------------------------------------------------------------
class PortalSessionHandler(SessionHandler):
"""
This object holds the sessions connected to the portal at any time.
It is synced with the server's equivalent SessionHandler over the AMP
connection.
Sessions register with the handler using the connect() method. This
will assign a new unique sessionid to the session and send that sessid
to the server using the AMP connection.
"""
def __init__(self):
"""
Init the handler
"""
self.portal = None
self.sessions = {}
self.latest_sessid = 0
self.uptime = time.time()
self.connection_time = 0
def at_server_connection(self):
"""
Called when the Portal establishes connection with the
Server. At this point, the AMP connection is already
established.
"""
self.connection_time = time.time()
def connect(self, session):
"""
Called by protocol at first connect. This adds a not-yet
authenticated session using an ever-increasing counter for sessid.
"""
self.latest_sessid += 1
sessid = self.latest_sessid
session.sessid = sessid
sessdata = session.get_sync_data()
self.sessions[sessid] = session
# sync with server-side
if self.portal.amp_protocol: # this is a timing issue
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation=PCONN,
data=sessdata)
def sync(self, session):
"""
Called by the protocol of an already connected session. This
can be used to sync the session info in a delayed manner,
such as when negotiation and handshakes are delayed.
"""
if session.sessid:
# only use if session already has sessid (i.e. has already connected)
sessdata = session.get_sync_data()
if self.portal.amp_protocol:
# we only send sessdata that should not have changed
# at the server level at this point
sessdata = dict((key, val) for key, val in sessdata.items() if key in ("protocol_key",
"address",
"sessid",
"suid",
"conn_time",
"protocol_flags",
"server_data",))
self.portal.amp_protocol.call_remote_ServerAdmin(session.sessid,
operation=PCONNSYNC,
data=sessdata)
def disconnect(self, session):
"""
Called from portal side when the connection is closed
from the portal side.
"""
sessid = session.sessid
if sessid in self.sessions:
del self.sessions[sessid]
del session
# tell server to also delete this session
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation=PDISCONN)
def server_connect(self, protocol_path="", config=dict()):
"""
Called by server to force the initialization of a new
protocol instance. Server wants this instance to get
a unique sessid and to be connected back as normal. This
is used to initiate irc/imc2/rss etc connections.
protocol_path - full python path to the class factory
for the protocol used, eg
'src.server.portal.irc.IRCClientFactory'
config - dictionary of configuration options, fed as **kwarg
to protocol class' __init__ method.
The called protocol class must have a method start()
that calls the portalsession.connect() as a normal protocol.
"""
global _MOD_IMPORT
if not _MOD_IMPORT:
from src.utils.utils import variable_from_module as _MOD_IMPORT
path, clsname = protocol_path.rsplit(".", 1)
cls = _MOD_IMPORT(path, clsname)
if not cls:
raise RuntimeError("ServerConnect: protocol factory '%s' not found." % protocol_path)
protocol = cls(self, **config)
protocol.start()
def server_disconnect(self, sessid, reason=""):
"""
Called by server to force a disconnect by sessid
"""
session = self.sessions.get(sessid, None)
if session:
session.disconnect(reason)
if sessid in self.sessions:
# in case sess.disconnect doesn't delete it
del self.sessions[sessid]
del session
def server_disconnect_all(self, reason=""):
"""
Called by server when forcing a clean disconnect for everyone.
"""
for session in self.sessions.values():
session.disconnect(reason)
del session
self.sessions = {}
def server_logged_in(self, sessid, data):
"""
The server tells us that the session has been
authenticated. Updated it.
"""
sess = self.get_session(sessid)
sess.load_sync_data(data)
def server_session_sync(self, serversessions):
"""
Server wants to save data to the portal, maybe because it's about
to shut down. We don't overwrite any sessions here, just update
them in-place and remove any that are out of sync (which should
normally not be the case)
serversessions - dictionary {sessid:{property:value},...} describing
the properties to sync on all sessions
"""
to_save = [sessid for sessid in serversessions if sessid in self.sessions]
to_delete = [sessid for sessid in self.sessions if sessid not in to_save]
# save protocols
for sessid in to_save:
self.sessions[sessid].load_sync_data(serversessions[sessid])
# disconnect out-of-sync missing protocols
for sessid in to_delete:
self.server_disconnect(sessid)
def count_loggedin(self, include_unloggedin=False):
"""
Count loggedin connections, alternatively count all connections.
"""
return len(self.get_sessions(include_unloggedin=include_unloggedin))
def session_from_suid(self, suid):
"""
Given a session id, retrieve the session (this is primarily
intended to be called by web clients)
"""
return [sess for sess in self.get_sessions(include_unloggedin=True)
if hasattr(sess, 'suid') and sess.suid == suid]
def data_in(self, session, text="", **kwargs):
"""
Called by portal sessions for relaying data coming
in from the protocol to the server. data is
serialized before passed on.
"""
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
msg=text,
data=kwargs)
def announce_all(self, message):
"""
Send message to all connection sessions
"""
for session in self.sessions.values():
session.data_out(message)
def data_out(self, sessid, text=None, **kwargs):
"""
Called by server for having the portal relay messages and data
to the correct session protocol.
"""
session = self.sessions.get(sessid, None)
if session:
session.data_out(text=text, **kwargs)
PORTAL_SESSIONS = PortalSessionHandler()

100
lib/server/portal/rss.py Normal file
View file

@ -0,0 +1,100 @@
"""
RSS parser for Evennia
This connects an RSS feed to an in-game Evennia channel, sending messages
to the channel whenever the feed updates.
"""
from twisted.internet import task, threads
from django.conf import settings
from src.server.session import Session
from src.utils import logger
RSS_ENABLED = settings.RSS_ENABLED
#RETAG = re.compile(r'<[^>]*?>')
if RSS_ENABLED:
try:
import feedparser
except ImportError:
raise ImportError("RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False.")
class RSSReader(Session):
"""
A simple RSS reader using universal feedparser
"""
def __init__(self, factory, url, rate):
self.url = url
self.rate = rate
self.factory = factory
self.old_entries = {}
def get_new(self):
"""Returns list of new items."""
feed = feedparser.parse(self.url)
new_entries = []
for entry in feed['entries']:
idval = entry['id'] + entry.get("updated", "")
if idval not in self.old_entries:
self.old_entries[idval] = entry
new_entries.append(entry)
return new_entries
def disconnect(self, reason=None):
"Disconnect from feed"
if self.factory.task and self.factory.task.running:
self.factory.task.stop()
self.sessionhandler.disconnect(self)
def _callback(self, new_entries, init):
"Called when RSS returns (threaded)"
if not init:
# for initialization we just ignore old entries
for entry in reversed(new_entries):
self.data_in("bot_data_in " + entry)
def data_in(self, text=None, **kwargs):
"Data RSS -> Server"
self.sessionhandler.data_in(self, text=text, **kwargs)
def _errback(self, fail):
"Report error"
logger.log_errmsg("RSS feed error: %s" % fail.value)
def update(self, init=False):
"Request feed"
return threads.deferToThread(self.get_new).addCallback(self._callback, init).addErrback(self._errback)
class RSSBotFactory(object):
"""
Initializes new bots
"""
def __init__(self, sessionhandler, uid=None, url=None, rate=None):
"Initialize"
self.sessionhandler = sessionhandler
self.url = url
self.rate = rate
self.uid = uid
self.bot = RSSReader(self, url, rate)
self.task = None
def start(self):
"""
Called by portalsessionhandler
"""
def errback(fail):
logger.log_errmsg(fail.value)
# set up session and connect it to sessionhandler
self.bot.init_session("rssbot", self.url, self.sessionhandler)
self.bot.uid = self.uid
self.bot.logged_in = True
self.sessionhandler.connect(self.bot)
# start repeater task
self.bot.update(init=True)
self.task = task.LoopingCall(self.bot.update)
if self.rate:
self.task.start(self.rate, now=False).addErrback(errback)

346
lib/server/portal/ssh.py Normal file
View file

@ -0,0 +1,346 @@
"""
This module implements the ssh (Secure SHell) protocol for encrypted
connections.
This depends on a generic session module that implements
the actual login procedure of the game, tracks
sessions etc.
Using standard ssh client,
"""
import os
from twisted.cred.checkers import credentials
from twisted.cred.portal import Portal
from twisted.conch.ssh.keys import Key
from twisted.conch.interfaces import IConchUser
from twisted.conch.ssh.userauth import SSHUserAuthServer
from twisted.conch.ssh import common
from twisted.conch.insults import insults
from twisted.conch.manhole_ssh import TerminalRealm, _Glue, ConchFactory
from twisted.conch.manhole import Manhole, recvline
from twisted.internet import defer
from twisted.conch import interfaces as iconch
from twisted.python import components
from django.conf import settings
from src.server import session
from src.players.models import PlayerDB
from src.utils import ansi, utils
ENCODINGS = settings.ENCODINGS
CTRL_C = '\x03'
CTRL_D = '\x04'
CTRL_BACKSLASH = '\x1c'
CTRL_L = '\x0c'
class SshProtocol(Manhole, session.Session):
"""
Each player connecting over ssh gets this protocol assigned to
them. All communication between game and player goes through
here.
"""
def __init__(self, starttuple):
"""
For setting up the player. If player is not None then we'll
login automatically.
"""
self.authenticated_player = starttuple[0]
# obs must not be called self.factory, that gets overwritten!
self.cfactory = starttuple[1]
def terminalSize(self, width, height):
"""
Initialize the terminal and connect to the new session.
"""
# Clear the previous input line, redraw it at the new
# cursor position
self.terminal.eraseDisplay()
self.terminal.cursorHome()
self.width = width
self.height = height
# initialize the session
client_address = self.getClientAddress()
self.init_session("ssh", client_address, self.cfactory.sessionhandler)
# since we might have authenticated already, we might set this here.
if self.authenticated_player:
self.logged_in = True
self.uid = self.authenticated_player.user.id
self.sessionhandler.connect(self)
def connectionMade(self):
"""
This is called when the connection is first
established.
"""
recvline.HistoricRecvLine.connectionMade(self)
self.keyHandlers[CTRL_C] = self.handle_INT
self.keyHandlers[CTRL_D] = self.handle_EOF
self.keyHandlers[CTRL_L] = self.handle_FF
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
# initalize
def handle_INT(self):
"""
Handle ^C as an interrupt keystroke by resetting the current input
variables to their initial state.
"""
self.lineBuffer = []
self.lineBufferIndex = 0
self.terminal.nextLine()
self.terminal.write("KeyboardInterrupt")
self.terminal.nextLine()
def handle_EOF(self):
"""
Handles EOF generally used to exit.
"""
if self.lineBuffer:
self.terminal.write('\a')
else:
self.handle_QUIT()
def handle_FF(self):
"""
Handle a 'form feed' byte - generally used to request a screen
refresh/redraw.
"""
self.terminal.eraseDisplay()
self.terminal.cursorHome()
def handle_QUIT(self):
"""
Quit, end, and lose the connection.
"""
self.terminal.loseConnection()
def connectionLost(self, reason=None):
"""
This is executed when the connection is lost for
whatever reason. It can also be called directly,
from the disconnect method.
"""
insults.TerminalProtocol.connectionLost(self, reason)
self.sessionhandler.disconnect(self)
self.terminal.loseConnection()
def getClientAddress(self):
"""
Returns the client's address and port in a tuple. For example
('127.0.0.1', 41917)
"""
return self.terminal.transport.getPeer()
def lineReceived(self, string):
"""
Communication Player -> Evennia. Any line return indicates a
command for the purpose of the MUD. So we take the user input
and pass it on to the game engine.
"""
self.sessionhandler.data_in(self, string)
def lineSend(self, string):
"""
Communication Evennia -> Player
Any string sent should already have been
properly formatted and processed
before reaching this point.
"""
for line in string.split('\n'):
#this is the telnet-specific method for sending
self.terminal.write(line)
self.terminal.nextLine()
# session-general method hooks
def disconnect(self, reason="Connection closed. Goodbye for now."):
"""
Disconnect from server
"""
if reason:
self.data_out(reason)
self.connectionLost(reason)
def data_out(self, text=None, **kwargs):
"""
Data Evennia -> Player access hook. 'data' argument is a dict
parsed for string settings.
ssh flags:
raw=True - leave all ansi markup and tokens unparsed
nomarkup=True - remove all ansi markup
"""
try:
text = utils.to_str(text if text else "", encoding=self.encoding)
except Exception, e:
self.lineSend(str(e))
return
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", False)
if raw:
self.lineSend(text)
else:
self.lineSend(ansi.parse_ansi(text.strip("{r") + "{r", strip_ansi=nomarkup))
class ExtraInfoAuthServer(SSHUserAuthServer):
def auth_password(self, packet):
"""
Password authentication.
Used mostly for setting up the transport so we can query
username and password later.
"""
password = common.getNS(packet[1:])[0]
c = credentials.UsernamePassword(self.user, password)
c.transport = self.transport
return self.portal.login(c, None, IConchUser).addErrback(
self._ebPassword)
class PlayerDBPasswordChecker(object):
"""
Checks the django db for the correct credentials for
username/password otherwise it returns the player or None which is
useful for the Realm.
"""
credentialInterfaces = (credentials.IUsernamePassword,)
def __init__(self, factory):
self.factory = factory
super(PlayerDBPasswordChecker, self).__init__()
def requestAvatarId(self, c):
"Generic credentials"
up = credentials.IUsernamePassword(c, None)
username = up.username
password = up.password
player = PlayerDB.objects.get_player_from_name(username)
res = (None, self.factory)
if player and player.user.check_password(password):
res = (player, self.factory)
return defer.succeed(res)
class PassAvatarIdTerminalRealm(TerminalRealm):
"""
Returns an avatar that passes the avatarId through to the
protocol. This is probably not the best way to do it.
"""
def _getAvatar(self, avatarId):
comp = components.Componentized()
user = self.userFactory(comp, avatarId)
sess = self.sessionFactory(comp)
sess.transportFactory = self.transportFactory
sess.chainedProtocolFactory = lambda: self.chainedProtocolFactory(avatarId)
comp.setComponent(iconch.IConchUser, user)
comp.setComponent(iconch.ISession, sess)
return user
class TerminalSessionTransport_getPeer:
"""
Taken from twisted's TerminalSessionTransport which doesn't
provide getPeer to the transport. This one does.
"""
def __init__(self, proto, chainedProtocol, avatar, width, height):
self.proto = proto
self.avatar = avatar
self.chainedProtocol = chainedProtocol
session = self.proto.session
self.proto.makeConnection(
_Glue(write=self.chainedProtocol.dataReceived,
loseConnection=lambda: avatar.conn.sendClose(session),
name="SSH Proto Transport"))
def loseConnection():
self.proto.loseConnection()
def getPeer():
session.conn.transport.transport.getPeer()
self.chainedProtocol.makeConnection(
_Glue(getPeer=getPeer, write=self.proto.write,
loseConnection=loseConnection,
name="Chained Proto Transport"))
self.chainedProtocol.terminalProtocol.terminalSize(width, height)
def getKeyPair(pubkeyfile, privkeyfile):
"""
This function looks for RSA keypair files in the current directory. If they
do not exist, the keypair is created.
"""
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
# No keypair exists. Generate a new RSA keypair
print " Generating SSH RSA keypair ...",
from Crypto.PublicKey import RSA
KEY_LENGTH = 1024
rsaKey = Key(RSA.generate(KEY_LENGTH))
publicKeyString = rsaKey.public().toString(type="OPENSSH")
privateKeyString = rsaKey.toString(type="OPENSSH")
# save keys for the future.
file(pubkeyfile, 'w+b').write(publicKeyString)
file(privkeyfile, 'w+b').write(privateKeyString)
print " done."
else:
publicKeyString = file(pubkeyfile).read()
privateKeyString = file(privkeyfile).read()
return Key.fromString(publicKeyString), Key.fromString(privateKeyString)
def makeFactory(configdict):
"""
Creates the ssh server factory.
"""
pubkeyfile = "ssh-public.key"
privkeyfile = "ssh-private.key"
def chainProtocolFactory(username=None):
return insults.ServerProtocol(
configdict['protocolFactory'],
*configdict.get('protocolConfigdict', (username,)),
**configdict.get('protocolKwArgs', {}))
rlm = PassAvatarIdTerminalRealm()
rlm.transportFactory = TerminalSessionTransport_getPeer
rlm.chainedProtocolFactory = chainProtocolFactory
factory = ConchFactory(Portal(rlm))
factory.sessionhandler = configdict['sessions']
try:
# create/get RSA keypair
publicKey, privateKey = getKeyPair(pubkeyfile, privkeyfile)
factory.publicKeys = {'ssh-rsa': publicKey}
factory.privateKeys = {'ssh-rsa': privateKey}
except Exception, e:
print " getKeyPair error: %(e)s\n WARNING: Evennia could not auto-generate SSH keypair. Using conch default keys instead." % {'e': e}
print " If this error persists, create game/%(pub)s and game/%(priv)s yourself using third-party tools." % {'pub': pubkeyfile, 'priv': privkeyfile}
factory.services = factory.services.copy()
factory.services['ssh-userauth'] = ExtraInfoAuthServer
factory.portal.registerChecker(PlayerDBPasswordChecker(factory))
return factory

84
lib/server/portal/ssl.py Normal file
View file

@ -0,0 +1,84 @@
"""
This is a simple context factory for auto-creating
SSL keys and certificates.
"""
import os
import sys
from twisted.internet import ssl as twisted_ssl
try:
import OpenSSL
except ImportError:
print " SSL_ENABLED requires PyOpenSSL."
sys.exit(5)
from src.server.portal.telnet import TelnetProtocol
class SSLProtocol(TelnetProtocol):
"""
Communication is the same as telnet, except data transfer
is done with encryption.
"""
pass
def verify_SSL_key_and_cert(keyfile, certfile):
"""
This function looks for RSA key and certificate in the current
directory. If files ssl.key and ssl.cert does not exist, they
are created.
"""
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
# key/cert does not exist. Create.
import subprocess
from Crypto.PublicKey import RSA
from twisted.conch.ssh.keys import Key
print " Creating SSL key and certificate ... ",
try:
# create the RSA key and store it.
KEY_LENGTH = 1024
rsaKey = Key(RSA.generate(KEY_LENGTH))
keyString = rsaKey.toString(type="OPENSSH")
file(keyfile, 'w+b').write(keyString)
except Exception, e:
print "rsaKey error: %(e)s\n WARNING: Evennia could not auto-generate SSL private key." % {'e': e}
print "If this error persists, create game/%(keyfile)s yourself using third-party tools." % {'keyfile': keyfile}
sys.exit(5)
# try to create the certificate
CERT_EXPIRE = 365 * 20 # twenty years validity
# default:
#openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300
exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (keyfile, certfile, CERT_EXPIRE)
#print "exestring:", exestring
try:
#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
subprocess.call(exestring)
except OSError, e:
string = "\n".join([
" %s\n" % e,
" Evennia's SSL context factory could not automatically",
" create an SSL certificate game/%(cert)s." % {'cert': certfile},
" A private key 'ssl.key' was already created. Please",
" create %(cert)s manually using the commands valid" % {'cert': certfile},
" for your operating system.",
" Example (linux, using the openssl program): ",
" %s" % exestring])
print string
sys.exit(5)
print "done."
def getSSLContext():
"""
Returns an SSL context (key and certificate). This function
verifies that key/cert exists before obtaining the context, and if
not, creates them.
"""
keyfile, certfile = "ssl.key", "ssl.cert"
verify_SSL_key_and_cert(keyfile, certfile)
return twisted_ssl.DefaultOpenSSLContextFactory(keyfile, certfile)

284
lib/server/portal/telnet.py Normal file
View file

@ -0,0 +1,284 @@
"""
This module implements the telnet protocol.
This depends on a generic session module that implements
the actual login procedure of the game, tracks
sessions etc.
"""
import re
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE, GA, WILL, WONT, ECHO
from src.server.session import Session
from src.server.portal import ttype, mssp, msdp, naws
from src.server.portal.mccp import Mccp, mccp_compress, MCCP
from src.server.portal.mxp import Mxp, mxp_parse
from src.utils import utils, ansi, logger
_RE_N = re.compile(r"\{n$")
_RE_LEND = re.compile(r"\n$|\r$", re.MULTILINE)
class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
"""
Each player connecting over telnet (ie using most traditional mud
clients) gets a telnet protocol instance assigned to them. All
communication between game and player goes through here.
"""
def connectionMade(self):
"""
This is called when the connection is first
established.
"""
# initialize the session
self.iaw_mode = False
self.no_lb_mode = False
client_address = self.transport.client
# this number is counted down for every handshake that completes.
# when it reaches 0 the portal/server syncs their data
self.handshakes = 6 # naws, ttype, mccp, mssp, msdp, mxp
self.init_session("telnet", client_address, self.factory.sessionhandler)
# negotiate client size
self.naws = naws.Naws(self)
# negotiate ttype (client info)
# Obs: mudlet ttype does not seem to work if we start mccp before ttype. /Griatch
self.ttype = ttype.Ttype(self)
# negotiate mccp (data compression) - turn this off for wireshark analysis
self.mccp = Mccp(self)
# negotiate mssp (crawler communication)
self.mssp = mssp.Mssp(self)
# msdp
self.msdp = msdp.Msdp(self)
# mxp support
self.mxp = Mxp(self)
# keepalive watches for dead links
self.transport.setTcpKeepAlive(1)
# add this new connection to sessionhandler so
# the Server becomes aware of it.
self.sessionhandler.connect(self)
# timeout the handshakes in case the client doesn't reply at all
from src.utils.utils import delay
delay(2, callback=self.handshake_done, retval=True)
def handshake_done(self, force=False):
"""
This is called by all telnet extensions once they are finished.
When all have reported, a sync with the server is performed.
The system will force-call this sync after a small time to handle
clients that don't reply to handshakes at all.
info - debug text from the protocol calling
"""
if self.handshakes > 0:
if force:
self.sessionhandler.sync(self)
return
self.handshakes -= 1
if self.handshakes <= 0:
# do the sync
self.sessionhandler.sync(self)
def enableRemote(self, option):
"""
This sets up the remote-activated options we allow for this protocol.
"""
pass
return (option == LINEMODE or
option == ttype.TTYPE or
option == naws.NAWS or
option == MCCP or
option == mssp.MSSP)
def enableLocal(self, option):
"""
Call to allow the activation of options for this protocol
"""
return (option == MCCP or option==ECHO)
def disableLocal(self, option):
"""
Disable a given option
"""
if option == ECHO:
return True
if option == MCCP:
self.mccp.no_mccp(option)
return True
else:
return super(TelnetProtocol, self).disableLocal(option)
def connectionLost(self, reason):
"""
this is executed when the connection is lost for
whatever reason. it can also be called directly, from
the disconnect method
"""
self.sessionhandler.disconnect(self)
self.transport.loseConnection()
def dataReceived(self, data):
"""
This method will split the incoming data depending on if it
starts with IAC (a telnet command) or not. All other data will
be handled in line mode. Some clients also sends an erroneous
line break after IAC, which we must watch out for.
OOB protocols (MSDP etc) already intercept subnegotiations
on their own, never entering this method. They will relay
their parsed data directly to self.data_in.
"""
if data and data[0] == IAC or self.iaw_mode:
try:
#print "IAC mode"
super(TelnetProtocol, self).dataReceived(data)
if len(data) == 1:
self.iaw_mode = True
else:
self.iaw_mode = False
return
except Exception, err1:
conv = ""
try:
for b in data:
conv += " " + repr(ord(b))
except Exception, err2:
conv = str(err2) + ":", str(data)
out = "Telnet Error (%s): %s (%s)" % (err1, data, conv)
logger.log_trace(out)
return
if self.no_lb_mode and _RE_LEND.match(data):
# we are in no_lb_mode and we get a single line break
# - this line break should have come with the previous
# command - it was already added so we drop it here
self.no_lb_mode = False
return
elif not _RE_LEND.search(data):
# no line break at the end of the command, note this.
data = data.rstrip("\r\n") + "\n"
self.no_lb_mode = True
# if we get to this point the command should end with a linebreak.
# We make sure to add it, to fix some clients messing this up.
#print "line data in:", repr(data)
StatefulTelnetProtocol.dataReceived(self, data)
def _write(self, data):
"hook overloading the one used in plain telnet"
# print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in data))
data = data.replace('\n', '\r\n').replace('\r\r\n', '\r\n')
#data = data.replace('\n', '\r\n')
super(TelnetProtocol, self)._write(mccp_compress(self, data))
def sendLine(self, line):
"hook overloading the one used by linereceiver"
#print "sendLine (%s):\n%s" % (self.state, line)
#escape IAC in line mode, and correctly add \r\n
line += self.delimiter
line = line.replace(IAC, IAC + IAC).replace('\n', '\r\n')
return self.transport.write(mccp_compress(self, line))
def lineReceived(self, string):
"""
Telnet method called when data is coming in over the telnet
connection. We pass it on to the game engine directly.
"""
self.data_in(text=string)
# Session hooks
def disconnect(self, reason=None):
"""
generic hook for the engine to call in order to
disconnect this protocol.
"""
if reason:
self.data_out(reason)
self.connectionLost(reason)
def data_in(self, text=None, **kwargs):
"""
Data Telnet -> Server
"""
self.sessionhandler.data_in(self, text=text, **kwargs)
def data_out(self, text=None, **kwargs):
"""
Data Evennia -> Player.
generic hook method for engine to call in order to send data
through the telnet connection.
valid telnet kwargs:
oob=<string> - supply an Out-of-Band instruction.
xterm256=True/False - enforce xterm256 setting. If not
given, ttype result is used. If
client does not suport xterm256, the
ansi fallback will be used
mxp=True/False - enforce mxp setting. If not given, enables if we
detected client support for it
ansi=True/False - enforce ansi setting. If not given,
ttype result is used.
nomarkup=True - strip all ansi markup (this is the same as
xterm256=False, ansi=False)
raw=True - pass string through without any ansi
processing (i.e. include Evennia ansi markers but do
not convert them into ansi tokens)
prompt=<string> - supply a prompt text which gets sent without a
newline added to the end
echo=True/False
The telnet ttype negotiation flags, if any, are used if no kwargs
are given.
"""
try:
text = utils.to_str(text if text else "", encoding=self.encoding)
except Exception, e:
self.sendLine(str(e))
return
if "oob" in kwargs:
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
if "MSDP" in self.protocol_flags:
for cmdname, args, kwargs in oobstruct:
#print "cmdname, args, kwargs:", cmdname, args, kwargs
msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs)
#print "msdp_string:", msdp_string
self.msdp.data_out(msdp_string)
# parse **kwargs, falling back to ttype if nothing is given explicitly
ttype = self.protocol_flags.get('TTYPE', {})
xterm256 = kwargs.get("xterm256", ttype.get('256 COLORS', False) if ttype.get("init_done") else True)
useansi = kwargs.get("ansi", ttype and ttype.get('ANSI', False) if ttype.get("init_done") else True)
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", not (xterm256 or useansi))
prompt = kwargs.get("prompt")
echo = kwargs.get("echo", None)
mxp = kwargs.get("mxp", self.protocol_flags.get("MXP", False))
#print "telnet kwargs=%s, message=%s" % (kwargs, text)
#print "xterm256=%s, useansi=%s, raw=%s, nomarkup=%s, init_done=%s" % (xterm256, useansi, raw, nomarkup, ttype.get("init_done"))
if raw:
# no processing whatsoever
self.sendLine(text)
elif text:
# we need to make sure to kill the color at the end in order
# to match the webclient output.
#print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self), "nomarkup: %s, xterm256: %s" % (nomarkup, xterm256)
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=xterm256, mxp=mxp)
if mxp:
linetosend = mxp_parse(linetosend)
self.sendLine(linetosend)
if prompt:
# Send prompt separately
prompt = ansi.parse_ansi(_RE_N.sub("", prompt) + "{n", strip_ansi=nomarkup, xterm256=xterm256)
if mxp:
prompt = mxp_parse(prompt)
prompt = prompt.replace(IAC, IAC + IAC).replace('\n', '\r\n')
prompt += IAC + GA
self.transport.write(mccp_compress(self, prompt))
if echo:
self.transport.write(mccp_compress(self, IAC+WONT+ECHO))
elif echo == False:
self.transport.write(mccp_compress(self, IAC+WILL+ECHO))

145
lib/server/portal/ttype.py Normal file
View file

@ -0,0 +1,145 @@
"""
TTYPE (MTTS) - Mud Terminal Type Standard
This module implements the TTYPE telnet protocol as per
http://tintin.sourceforge.net/mtts/. It allows the server to ask the
client about its capabilities. If the client also supports TTYPE, it
will return with information such as its name, if it supports colour
etc. If the client does not support TTYPE, this will be ignored.
All data will be stored on the protocol's protocol_flags dictionary,
under the 'TTYPE' key.
"""
# telnet option codes
TTYPE = chr(24)
IS = chr(0)
SEND = chr(1)
# terminal capabilities and their codes
MTTS = [(128, 'PROXY'),
(64, 'SCREEN READER'),
(32, 'OSC COLOR PALETTE'),
(16, 'MOUSE TRACKING'),
(8, '256 COLORS'),
(4, 'UTF-8'),
(2, 'VT100'),
(1, 'ANSI')]
class Ttype(object):
"""
Handles ttype negotiations. Called and initiated by the
telnet protocol.
"""
def __init__(self, protocol):
"""
initialize ttype by storing protocol on ourselves and calling
the client to see if it supporst ttype.
the ttype_step indicates how far in the data retrieval we've
gotten.
"""
self.ttype_step = 0
self.protocol = protocol
self.protocol.protocol_flags['TTYPE'] = {"init_done": False}
# is it a safe bet to assume ANSI is always supported?
self.protocol.protocol_flags['TTYPE']['ANSI'] = True
# setup protocol to handle ttype initialization and negotiation
self.protocol.negotiationMap[TTYPE] = self.will_ttype
# ask if client will ttype, connect callback if it does.
self.protocol.do(TTYPE).addCallbacks(self.will_ttype, self.wont_ttype)
def wont_ttype(self, option):
"""
Callback if ttype is not supported by client.
"""
self.protocol.protocol_flags['TTYPE']["init_done"] = True
self.protocol.handshake_done()
def will_ttype(self, option):
"""
Handles negotiation of the ttype protocol once the
client has confirmed that it will respond with the ttype
protocol.
The negotiation proceeds in several steps, each returning a
certain piece of information about the client. All data is
stored on protocol.protocol_flags under the TTYPE key.
"""
options = self.protocol.protocol_flags.get('TTYPE')
if options and options.get('init_done') or self.ttype_step > 3:
return
try:
option = "".join(option).lstrip(IS)
except TypeError:
pass
#print "incoming TTYPE option:", option
if self.ttype_step == 0:
# just start the request chain
self.protocol.requestNegotiation(TTYPE, SEND)
elif self.ttype_step == 1:
# this is supposed to be the name of the client/terminal.
# For clients not supporting the extended TTYPE
# definition, subsequent calls will just repeat-return this.
clientname = option.upper()
# use name to identify support for xterm256. Many of these
# only support after a certain version, but all support
# it since at least 4 years. We assume recent client here for now.
xterm256 = False
if clientname.startswith("MUDLET"):
# supports xterm256 stably since 1.1 (2010?)
xterm256 = clientname.split("MUDLET",1)[1].strip() >= "1.1"
else:
xterm256 = (clientname.startswith("XTERM") or
clientname.endswith("-256COLOR") or
clientname in ("ATLANTIS", # > 0.9.9.0 (aug 2009)
"CMUD", # > 3.04 (mar 2009)
"KILDCLIENT", # > 2.2.0 (sep 2005)
"MUDLET", # > beta 15 (sep 2009)
"MUSHCLIENT", # > 4.02 (apr 2007)
"PUTTY", # > 0.58 (apr 2005)
"BEIP")) # > 2.00.206 (late 2009) (BeipMu)
# all clients supporting TTYPE at all seem to support ANSI
self.protocol.protocol_flags['TTYPE']['ANSI'] = True
self.protocol.protocol_flags['TTYPE']['256 COLORS'] = xterm256
self.protocol.protocol_flags['TTYPE']['CLIENTNAME'] = clientname
self.protocol.requestNegotiation(TTYPE, SEND)
elif self.ttype_step == 2:
# this is a term capabilities flag
term = option
# identify xterm256 based on flag
xterm256 = (term.endswith("-256color") # Apple Terminal, old Tintin
or term.endswith("xterm") and # old Tintin, Putty
not term.endswith("-color"))
if xterm256:
self.protocol.protocol_flags['TTYPE']['ANSI'] = True
self.protocol.protocol_flags['TTYPE']['256 COLORS'] = xterm256
self.protocol.protocol_flags['TTYPE']['TERM'] = term
# request next information
self.protocol.requestNegotiation(TTYPE, SEND)
elif self.ttype_step == 3:
# the MTTS bitstring identifying term capabilities
if option.startswith("MTTS"):
option = option.split(" ")[1]
if option.isdigit():
# a number - determine the actual capabilities
option = int(option)
support = dict((capability, True) for bitval, capability in MTTS if option & bitval > 0)
self.protocol.protocol_flags['TTYPE'].update(support)
else:
# some clients send erroneous MTTS as a string. Add directly.
self.protocol.protocol_flags['TTYPE'][option.upper()] = True
self.protocol.protocol_flags['TTYPE']['init_done'] = True
# print "TTYPE final:", self.protocol.protocol_flags['TTYPE']
# we must sync ttype once it'd done
self.protocol.handshake_done()
self.ttype_step += 1

View file

@ -0,0 +1,252 @@
"""
Web client server resource.
The Evennia web client consists of two components running
on twisted and django. They are both a part of the Evennia
website url tree (so the testing website might be located
on http://localhost:8000/, whereas the webclient can be
found on http://localhost:8000/webclient.)
/webclient - this url is handled through django's template
system and serves the html page for the client
itself along with its javascript chat program.
/webclientdata - this url is called by the ajax chat using
POST requests (long-polling when necessary)
The WebClient resource in this module will
handle these requests and act as a gateway
to sessions connected over the webclient.
"""
import time
import json
from hashlib import md5
from twisted.web import server, resource
from django.utils.functional import Promise
from django.utils.encoding import force_unicode
from django.conf import settings
from src.utils import utils, logger
from src.utils.text2html import parse_html
from src.server import session
SERVERNAME = settings.SERVERNAME
ENCODINGS = settings.ENCODINGS
# defining a simple json encoder for returning
# django data to the client. Might need to
# extend this if one wants to send more
# complex database objects too.
class LazyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Promise):
return force_unicode(obj)
return super(LazyEncoder, self).default(obj)
def jsonify(obj):
return utils.to_str(json.dumps(obj, ensure_ascii=False, cls=LazyEncoder))
#
# WebClient resource - this is called by the ajax client
# using POST requests to /webclientdata.
#
class WebClient(resource.Resource):
"""
An ajax/comet long-polling transport
"""
isLeaf = True
allowedMethods = ('POST',)
def __init__(self):
self.requests = {}
self.databuffer = {}
#def getChild(self, path, request):
# """
# This is the place to put dynamic content.
# """
# return self
def _responseFailed(self, failure, suid, request):
"callback if a request is lost/timed out"
try:
del self.requests[suid]
except KeyError:
pass
def lineSend(self, suid, string, data=None):
"""
This adds the data to the buffer and/or sends it to
the client as soon as possible.
"""
request = self.requests.get(suid)
if request:
# we have a request waiting. Return immediately.
request.write(jsonify({'msg': string, 'data': data}))
request.finish()
del self.requests[suid]
else:
# no waiting request. Store data in buffer
dataentries = self.databuffer.get(suid, [])
dataentries.append(jsonify({'msg': string, 'data': data}))
self.databuffer[suid] = dataentries
def client_disconnect(self, suid):
"""
Disconnect session with given suid.
"""
if suid in self.requests:
self.requests[suid].finish()
del self.requests[suid]
if suid in self.databuffer:
del self.databuffer[suid]
def mode_init(self, request):
"""
This is called by render_POST when the client
requests an init mode operation (at startup)
"""
#csess = request.getSession() # obs, this is a cookie, not
# an evennia session!
#csees.expireCallbacks.append(lambda : )
suid = request.args.get('suid', ['0'])[0]
remote_addr = request.getClientIP()
host_string = "%s (%s:%s)" % (SERVERNAME, request.getRequestHostname(), request.getHost().port)
if suid == '0':
# creating a unique id hash string
suid = md5(str(time.time())).hexdigest()
self.databuffer[suid] = []
sess = WebClientSession()
sess.client = self
sess.init_session("webclient", remote_addr, self.sessionhandler)
sess.suid = suid
sess.sessionhandler.connect(sess)
return jsonify({'msg': host_string, 'suid': suid})
def mode_input(self, request):
"""
This is called by render_POST when the client
is sending data to the server.
"""
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
return ''
sess = self.sessionhandler.session_from_suid(suid)
if sess:
sess = sess[0]
text = request.args.get('msg', [''])[0]
data = request.args.get('data', [None])[0]
sess.sessionhandler.data_in(sess, text, data=data)
return ''
def mode_receive(self, request):
"""
This is called by render_POST when the client is telling us
that it is ready to receive data as soon as it is
available. This is the basis of a long-polling (comet)
mechanism: the server will wait to reply until data is
available.
"""
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
return ''
dataentries = self.databuffer.get(suid, [])
if dataentries:
return dataentries.pop(0)
request.notifyFinish().addErrback(self._responseFailed, suid, request)
if suid in self.requests:
self.requests[suid].finish() # Clear any stale request.
self.requests[suid] = request
return server.NOT_DONE_YET
def mode_close(self, request):
"""
This is called by render_POST when the client is signalling
that it is about to be closed.
"""
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
self.client_disconnect(suid)
else:
try:
sess = self.sessionhandler.session_from_suid(suid)[0]
sess.sessionhandler.disconnect(sess)
except IndexError:
self.client_disconnect(suid)
pass
return ''
def render_POST(self, request):
"""
This function is what Twisted calls with POST requests coming
in from the ajax client. The requests should be tagged with
different modes depending on what needs to be done, such as
initializing or sending/receving data through the request. It
uses a long-polling mechanism to avoid sending data unless
there is actual data available.
"""
dmode = request.args.get('mode', [None])[0]
if dmode == 'init':
# startup. Setup the server.
return self.mode_init(request)
elif dmode == 'input':
# input from the client to the server
return self.mode_input(request)
elif dmode == 'receive':
# the client is waiting to receive data.
return self.mode_receive(request)
elif dmode == 'close':
# the client is closing
return self.mode_close(request)
else:
# this should not happen if client sends valid data.
return ''
#
# A session type handling communication over the
# web client interface.
#
class WebClientSession(session.Session):
"""
This represents a session running in a webclient.
"""
def disconnect(self, reason=None):
"""
Disconnect from server
"""
if reason:
self.client.lineSend(self.suid, reason)
self.client.client_disconnect(self.suid)
def data_out(self, text=None, **kwargs):
"""
Data Evennia -> Player access hook.
webclient flags checked are
raw=True - no parsing at all (leave ansi-to-html markers unparsed)
nomarkup=True - clean out all ansi/html markers and tokens
"""
# string handling is similar to telnet
try:
text = utils.to_str(text if text else "", encoding=self.encoding)
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", False)
if raw:
self.client.lineSend(self.suid, text)
else:
self.client.lineSend(self.suid,
parse_html(text, strip_ansi=nomarkup))
return
except Exception:
logger.log_trace()

View file

@ -0,0 +1,135 @@
"""
Websocket-webclient
This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is
used together with src/web/media/javascript/evennia_websocket_webclient.js.
Thanks to Ricard Pillosu whose Evennia plugin inspired this module.
Communication over the websocket interface is done with normal text
communication. A special case is OOB-style communication; to do this
the client must send data on the following form:
OOB{"func1":[args], "func2":[args], ...}
where the dict is JSON encoded. The initial OOB-prefix
is used to identify this type of communication, all other data
is considered plain text (command input).
Example of call from a javascript client:
websocket = new WeSocket("ws://localhost:8021")
var msg1 = "WebSocket Test"
websocket.send(msg1)
var msg2 = JSON.stringify({"testfunc":[[1,2,3], {"kwarg":"val"}]})
websocket.send("OOB" + msg2)
websocket.close()
"""
import json
from twisted.internet.protocol import Protocol
from src.server.session import Session
from src.utils.logger import log_trace
from src.utils.utils import to_str, make_iter
from src.utils.text2html import parse_html
class WebSocketClient(Protocol, Session):
"""
Implements the server-side of the Websocket connection.
"""
def connectionMade(self):
"""
This is called when the connection is first established.
"""
client_address = self.transport.client
self.init_session("websocket", client_address, self.factory.sessionhandler)
# watch for dead links
self.transport.setTcpKeepAlive(1)
self.sessionhandler.connect(self)
def disconnect(self, reason=None):
"""
generic hook for the engine to call in order to
disconnect this protocol.
"""
if reason:
self.data_out(text=reason)
self.connectionLost(reason)
def connectionLost(self, reason):
"""
this is executed when the connection is lost for
whatever reason. it can also be called directly, from
the disconnect method
"""
self.sessionhandler.disconnect(self)
self.transport.close()
def dataReceived(self, string):
"""
Method called when data is coming in over
the websocket connection.
Type of data is identified by a 3-character
prefix.
OOB - This is an Out-of-band instruction. If so,
the remaining string should be a json-packed
string on the form {oobfuncname: [args, ], ...}
any other prefix (or lack of prefix) is considered
plain text data, to be treated like a game
input command.
"""
if string[:3] == "OOB":
string = string[3:]
try:
oobdata = json.loads(string)
for (key, args) in oobdata.items():
#print "oob data in:", (key, args)
self.data_in(text=None, oob=(key, make_iter(args)))
except Exception:
log_trace("Websocket malformed OOB request: %s" % string)
else:
# plain text input
self.data_in(text=string)
def sendLine(self, line):
"send data to client"
return self.transport.write(line)
def data_in(self, text=None, **kwargs):
"""
Data Websocket -> Server
"""
self.sessionhandler.data_in(self, text=text, **kwargs)
def data_out(self, text=None, **kwargs):
"""
Data Evennia -> Player.
generic hook method for engine to call in order to send data
through the websocket connection.
valid webclient kwargs:
oob=<string> - supply an Out-of-Band instruction.
raw=True - no parsing at all (leave ansi-to-html markers unparsed)
nomarkup=True - clean out all ansi/html markers and tokens
"""
try:
text = to_str(text if text else "", encoding=self.encoding)
except Exception, e:
self.sendLine(str(e))
if "oob" in kwargs:
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
#print "oob data_out:", "OOB" + json.dumps(oobstruct)
self.sendLine("OOB" + json.dumps(oobstruct))
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", False)
if "prompt" in kwargs:
self.sendLine("PROMPT" + parse_html(kwargs["prompt"], strip_ansi=nomarkup))
if raw:
self.sendLine(text)
else:
self.sendLine(parse_html(text, strip_ansi=nomarkup))

504
lib/server/server.py Normal file
View file

@ -0,0 +1,504 @@
"""
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 time
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.abspath(__file__)))))
from twisted.web import server, static
from twisted.application import internet, service
from twisted.internet import reactor, defer
import django
django.setup()
from django.db import connection
from django.conf import settings
from src.players.models import PlayerDB
from src.scripts.models import ScriptDB
from src.server.models import ServerConfig
from src.server import initial_setup
from src.utils.utils import get_evennia_version, mod_import, make_iter
from src.comms import channelhandler
from src.server.sessionhandler import SESSIONS
# setting up server-side field cache
from django.db.models.signals import post_save
from src.server.caches import field_post_save
#pre_save.connect(field_pre_save, dispatch_uid="fieldcache")
post_save.connect(field_post_save, dispatch_uid="fieldcache")
#from src.server.caches import post_attr_update
#from django.db.models.signals import m2m_changed
# connect to attribute cache signal
#m2m_changed.connect(post_attr_update, sender=TypedObject.db_attributes.through)
_SA = object.__setattr__
if os.name == 'nt':
# For Windows we need to handle pid files manually.
SERVER_PIDFILE = os.path.join(settings.GAME_DIR, 'server.pid')
# a file with a flag telling the server to restart after shutdown or not.
SERVER_RESTART = os.path.join(settings.GAME_DIR, 'server.restart')
# module containing hook methods called during start_stop
SERVER_STARTSTOP_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE)
# module containing plugin services
SERVER_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)]
#------------------------------------------------------------
# Evennia Server settings
#------------------------------------------------------------
SERVERNAME = settings.SERVERNAME
VERSION = get_evennia_version()
AMP_ENABLED = True
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
AMP_INTERFACE = settings.AMP_INTERFACE
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
GUEST_ENABLED = settings.GUEST_ENABLED
# server-channel mappings
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
IMC2_ENABLED = settings.IMC2_ENABLED
IRC_ENABLED = settings.IRC_ENABLED
RSS_ENABLED = settings.RSS_ENABLED
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
#------------------------------------------------------------
# Evennia Main Server object
#------------------------------------------------------------
class Evennia(object):
"""
The main Evennia server handler. This object sets up the database and
tracks and interlinks all the twisted network services that make up
evennia.
"""
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 = SESSIONS
self.sessions.server = self
# Database-specific startup optimizations.
self.sqlite3_prep()
# Run the initial setup if needed
self.run_initial_setup()
self.start_time = time.time()
# initialize channelhandler
channelhandler.CHANNELHANDLER.update()
# 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 = True
self.run_init_hooks()
# Server startup methods
def sqlite3_prep(self):
"""
Optimize some SQLite stuff at startup since we
can't save it to the database.
"""
if ((".".join(str(i) for i in django.VERSION) < "1.2" and settings.DATABASE_ENGINE == "sqlite3")
or (hasattr(settings, 'DATABASES')
and settings.DATABASES.get("default", {}).get('ENGINE', None)
== 'django.db.backends.sqlite3')):
cursor = connection.cursor()
cursor.execute("PRAGMA cache_size=10000")
cursor.execute("PRAGMA synchronous=OFF")
cursor.execute("PRAGMA count_changes=OFF")
cursor.execute("PRAGMA temp_store=2")
def update_defaults(self):
"""
We make sure to store the most important object defaults here, so
we can catch if they change and update them on-objects automatically.
This allows for changing default cmdset locations and default
typeclasses in the settings file and have them auto-update all
already existing objects.
"""
# setting names
settings_names = ("CMDSET_CHARACTER", "CMDSET_PLAYER",
"BASE_PLAYER_TYPECLASS", "BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS",
"BASE_CHANNEL_TYPECLASS")
# get previous and current settings so they can be compared
settings_compare = zip([ServerConfig.objects.conf(name) for name in settings_names],
[settings.__getattr__(name) for name in settings_names])
mismatches = [i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]]
if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any()
# we have a changed default. Import relevant objects and
# run the update
from src.objects.models import ObjectDB
from src.comms.models import ChannelDB
#from src.players.models import PlayerDB
for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches):
# update the database
print " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (settings_names[i], prev, curr)
if i == 0:
[obj.__setattr__("cmdset_storage", curr) for obj in ObjectDB.objects.filter(db_cmdset_storage__exact=prev)]
if i == 1:
[ply.__setattr__("cmdset_storage", curr) for ply in PlayerDB.objects.filter(db_cmdset_storage__exact=prev)]
if i == 2:
[ply.__setattr__("typeclass_path", curr) for ply in PlayerDB.objects.filter(db_typeclass_path__exact=prev)]
if i in (3, 4, 5, 6):
[obj.__setattr__("typeclass_path", curr) for obj in ObjectDB.objects.filter(db_typeclass_path__exact=prev)]
if i == 7:
[scr.__setattr__("typeclass_path", curr) for scr in ScriptDB.objects.filter(db_typeclass_path__exact=prev)]
if i == 8:
[scr.__setattr__("typeclass_path", curr) for scr in ChannelDB.objects.filter(db_typeclass_path__exact=prev)]
# store the new default and clean caches
ServerConfig.objects.conf(settings_names[i], curr)
ObjectDB.flush_instance_cache()
PlayerDB.flush_instance_cache()
ScriptDB.flush_instance_cache()
ChannelDB.flush_instance_cache()
# if this is the first start we might not have a "previous"
# setup saved. Store it now.
[ServerConfig.objects.conf(settings_names[i], tup[1])
for i, tup in enumerate(settings_compare) if not tup[0]]
def run_initial_setup(self):
"""
This attempts to run the initial_setup script of the server.
It returns if this is not the first time the server starts.
Once finished the last_initial_setup_step is set to -1.
"""
last_initial_setup_step = ServerConfig.objects.conf('last_initial_setup_step')
if not last_initial_setup_step:
# None is only returned if the config does not exist,
# i.e. this is an empty DB that needs populating.
print ' Server started for the first time. Setting defaults.'
initial_setup.handle_setup(0)
print '-' * 50
elif int(last_initial_setup_step) >= 0:
# a positive value means the setup crashed on one of its
# modules and setup will resume from this step, retrying
# the last failed module. When all are finished, the step
# is set to -1 to show it does not need to be run again.
print ' Resuming initial setup from step %(last)s.' % \
{'last': last_initial_setup_step}
initial_setup.handle_setup(int(last_initial_setup_step))
print '-' * 50
def run_init_hooks(self):
"""
Called every server start
"""
from src.objects.models import ObjectDB
#from src.players.models import PlayerDB
#update eventual changed defaults
self.update_defaults()
#print "run_init_hooks:", ObjectDB.get_all_cached_instances()
[o.at_init() for o in ObjectDB.get_all_cached_instances()]
[p.at_init() for p in PlayerDB.get_all_cached_instances()]
with open(SERVER_RESTART, 'r') as f:
mode = f.read()
if mode in ('True', 'reload'):
from src.server.oobhandler import OOB_HANDLER
OOB_HANDLER.restore()
from src.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.restore()
# call correct server hook based on start file value
if mode in ('True', 'reload'):
# True was the old reload flag, kept for compatibilty
self.at_server_reload_start()
elif mode in ('reset', 'shutdown'):
self.at_server_cold_start()
# clear eventual lingering session storages
ObjectDB.objects.clear_all_sessids()
# always call this regardless of start type
self.at_server_start()
def set_restart_mode(self, mode=None):
"""
This manages the flag file that tells the runner if the server is
reloading, resetting or shutting down. Valid modes are
'reload', 'reset', 'shutdown' and None.
If mode is None, no change will be done to the flag file.
Either way, the active restart setting (Restart=True/False) is
returned so the server knows which more it's in.
"""
if mode is None:
with open(SERVER_RESTART, 'r') as f:
# mode is either shutdown, reset or reload
mode = f.read()
else:
with open(SERVER_RESTART, 'w') as f:
f.write(str(mode))
return mode
@defer.inlineCallbacks
def shutdown(self, mode=None, _reactor_stopping=False):
"""
Shuts down the server from inside it.
mode - sets the server restart mode.
'reload' - server restarts, no "persistent" scripts
are stopped, at_reload hooks called.
'reset' - server restarts, non-persistent scripts stopped,
at_shutdown hooks called.
'shutdown' - like reset, but server will not auto-restart.
None - keep currently set flag from flag file.
_reactor_stopping - this is set if server is stopped by a kill
command OR this method was already called
once - in both cases the reactor is
dead/stopping already.
"""
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# this means we have already passed through this method
# once; we don't need to run the shutdown procedure again.
defer.returnValue(None)
mode = self.set_restart_mode(mode)
# call shutdown hooks on all cached objects
from src.objects.models import ObjectDB
#from src.players.models import PlayerDB
from src.server.models import ServerConfig
if mode == 'reload':
# call restart hooks
yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()]
yield [p.at_server_reload() for p in PlayerDB.get_all_cached_instances()]
yield [(s.pause(), s.at_server_reload())
for s in ScriptDB.get_all_cached_instances()]
yield self.sessions.all_sessions_portal_sync()
ServerConfig.objects.conf("server_restart_mode", "reload")
from src.server.oobhandler import OOB_HANDLER
OOB_HANDLER.save()
from src.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.save()
self.at_server_reload_stop()
else:
if mode == 'reset':
# don't unset the is_connected flag on reset, otherwise
# same as shutdown
yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()]
yield [p.at_server_shutdown() for p in PlayerDB.get_all_cached_instances()]
else: # shutdown
yield [_SA(p, "is_connected", False) for p in PlayerDB.get_all_cached_instances()]
yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()]
yield [(p.unpuppet_all(), p.at_server_shutdown())
for p in PlayerDB.get_all_cached_instances()]
yield [s.at_server_shutdown() for s in ScriptDB.get_all_cached_instances()]
yield ObjectDB.objects.clear_all_sessids()
ServerConfig.objects.conf("server_restart_mode", "reset")
self.at_server_cold_stop()
# stopping time
from src.utils import gametime
gametime.save()
self.at_server_stop()
# if _reactor_stopping is true, reactor does not need to
# be stopped again.
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
# for Windows we need to remove pid files manually
os.remove(SERVER_PIDFILE)
if not _reactor_stopping:
# this will also send a reactor.stop signal, so we set a
# flag to avoid loops.
self.shutdown_complete = True
reactor.callLater(0, reactor.stop)
# server start/stop hooks
def at_server_start(self):
"""
This is called every time the server starts up, regardless of
how it was shut down.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_start()
def at_server_stop(self):
"""
This is called just before a server is shut down, regardless
of it is fore a reload, reset or shutdown.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_stop()
def at_server_reload_start(self):
"""
This is called only when server starts back up after a reload.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_reload_start()
def at_server_reload_stop(self):
"""
This is called only time the server stops before a reload.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_reload_stop()
def at_server_cold_start(self):
"""
This is called only when the server starts "cold", i.e. after a
shutdown or a reset.
"""
if GUEST_ENABLED:
for guest in PlayerDB.objects.all().filter(db_typeclass_path=settings.BASE_GUEST_TYPECLASS):
for character in filter(None, guest.db._playable_characters):
character.delete()
guest.delete()
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_cold_start()
def at_server_cold_stop(self):
"""
This is called only when the server goes down due to a shutdown or reset.
"""
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_cold_stop()
#------------------------------------------------------------
#
# Start the Evennia game server and add all active services
#
#------------------------------------------------------------
# Tell the system the server is starting up; some things are not available yet
ServerConfig.objects.conf("server_starting_mode", True)
# twistd requires us to define the variable 'application' so it knows
# what to execute from.
application = service.Application('Evennia')
# The main evennia server program. This sets up the database
# and is where we store all the other services.
EVENNIA = Evennia(application)
print '-' * 50
print ' %(servername)s Server (%(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.
ifacestr = ""
if AMP_INTERFACE != '127.0.0.1':
ifacestr = "-%s" % AMP_INTERFACE
print ' amp (to Portal)%s: %s' % (ifacestr, AMP_PORT)
from src.server import amp
factory = amp.AmpServerFactory(EVENNIA)
amp_service = internet.TCPServer(AMP_PORT, factory, interface=AMP_INTERFACE)
amp_service.setName("EvenniaPortal")
EVENNIA.services.addService(amp_service)
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(minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
maxthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[1]))
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
# point our static resources to url /static
web_root.putChild("static", static.File(settings.STATIC_ROOT))
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
for proxyport, serverport in WEBSERVER_PORTS:
# create the webserver (we only need the port for this)
webserver = WSGIWebServer(threads, serverport, web_site, interface='127.0.0.1')
webserver.setName('EvenniaWebServer%s' % serverport)
EVENNIA.services.addService(webserver)
print " webserver: %s" % serverport
ENABLED = []
if IRC_ENABLED:
# IRC channel connections
ENABLED.append('irc')
if IMC2_ENABLED:
# IMC2 channel connections
ENABLED.append('imc2')
if RSS_ENABLED:
# RSS feed channel connections
ENABLED.append('rss')
if ENABLED:
print " " + ", ".join(ENABLED) + " enabled."
for plugin_module in SERVER_SERVICES_PLUGIN_MODULES:
# external plugin protocols
plugin_module.start_plugin_services(EVENNIA)
print '-' * 50 # end of terminal output
# clear server startup mode
ServerConfig.objects.conf("server_starting_mode", delete=True)
if os.name == 'nt':
# Windows only: Set PID file manually
f = open(os.path.join(settings.GAME_DIR, 'server.pid'), 'w')
f.write(str(os.getpid()))
f.close()

335
lib/server/serversession.py Normal file
View file

@ -0,0 +1,335 @@
"""
This defines a the Server's generic session object. This object represents
a connection to the outside world but don't know any details about how the
connection actually happens (so it's the same for telnet, web, ssh etc).
It is stored on the Server side (as opposed to protocol-specific sessions which
are stored on the Portal side)
"""
import time
from datetime import datetime
from django.conf import settings
#from src.scripts.models import ScriptDB
from src.comms.models import ChannelDB
from src.utils import logger, utils
from src.utils.inlinefunc import parse_inlinefunc
from src.utils.utils import make_iter
from src.commands.cmdhandler import cmdhandler
from src.commands.cmdsethandler import CmdSetHandler
from src.server.session import Session
IDLE_COMMAND = settings.IDLE_COMMAND
_GA = object.__getattribute__
_ObjectDB = None
_OOB_HANDLER = None
# load optional out-of-band function module (this acts as a verification)
OOB_PLUGIN_MODULES = [utils.mod_import(mod)
for mod in make_iter(settings.OOB_PLUGIN_MODULES) if mod]
INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
# i18n
from django.utils.translation import ugettext as _
#------------------------------------------------------------
# Server Session
#------------------------------------------------------------
class ServerSession(Session):
"""
This class represents a player's session and is a template for
individual protocols to communicate with Evennia.
Each player gets a session assigned to them whenever they connect
to the game server. All communication between game and player goes
through their session.
"""
def __init__(self):
"Initiate to avoid AttributeErrors down the line"
self.puppet = None
self.player = None
self.cmdset_storage_string = ""
self.cmdset = CmdSetHandler(self, True)
def __cmdset_storage_get(self):
return [path.strip() for path in self.cmdset_storage_string.split(',')]
def __cmdset_storage_set(self, value):
self.cmdset_storage_string = ",".join(str(val).strip() for val in make_iter(value))
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)
def at_sync(self):
"""
This is called whenever a session has been resynced with the portal.
At this point all relevant attributes have already been set and
self.player been assigned (if applicable).
Since this is often called after a server restart we need to set up
the session as it was.
"""
global _ObjectDB
if not _ObjectDB:
from src.objects.models import ObjectDB as _ObjectDB
if not self.logged_in:
# assign the unloggedin-command set.
self.cmdset_storage = settings.CMDSET_UNLOGGEDIN
self.cmdset.update(init_mode=True)
if self.puid:
# reconnect puppet (puid is only set if we are coming
# back from a server reload)
obj = _ObjectDB.objects.get(id=self.puid)
self.player.puppet_object(self.sessid, obj, normal_mode=False)
def at_login(self, player):
"""
Hook called by sessionhandler when the session becomes authenticated.
player - the player associated with the session
"""
self.player = player
self.uid = self.player.id
self.uname = self.player.username
self.logged_in = True
self.conn_time = time.time()
self.puid = None
self.puppet = None
self.cmdset_storage = settings.CMDSET_SESSION
# Update account's last login time.
self.player.last_login = datetime.now()
self.player.save()
# add the session-level cmdset
self.cmdset = CmdSetHandler(self, True)
def at_disconnect(self):
"""
Hook called by sessionhandler when disconnecting this session.
"""
if self.logged_in:
sessid = self.sessid
player = self.player
player.unpuppet_object(sessid)
uaccount = player
uaccount.last_login = datetime.now()
uaccount.save()
# calling player hook
player.at_disconnect()
self.logged_in = False
if not self.sessionhandler.sessions_from_player(player):
# no more sessions connected to this player
player.is_connected = False
# this may be used to e.g. delete player after disconnection etc
player.at_post_disconnect()
def get_player(self):
"""
Get the player associated with this session
"""
return self.logged_in and self.player
def get_puppet(self):
"""
Returns the in-game character associated with this session.
This returns the typeclass of the object.
"""
return self.logged_in and self.puppet
get_character = get_puppet
def get_puppet_or_player(self):
"""
Returns session if not logged in; puppet if one exists,
otherwise return the player.
"""
if self.logged_in:
return self.puppet if self.puppet else self.player
return None
def log(self, message, channel=True):
"""
Emits session info to the appropriate outputs and info channels.
"""
if channel:
try:
cchan = settings.CHANNEL_CONNECTINFO
cchan = ChannelDB.objects.get_channel(cchan[0])
cchan.msg("[%s]: %s" % (cchan.key, message))
except Exception:
pass
logger.log_infomsg(message)
def get_client_size(self):
"""
Return eventual eventual width and height reported by the
client. Note that this currently only deals with a single
client window (windowID==0) as in traditional telnet session
"""
flags = self.protocol_flags
width = flags.get('SCREENWIDTH', {}).get(0, settings.CLIENT_DEFAULT_WIDTH)
height = flags.get('SCREENHEIGHT', {}).get(0, settings.CLIENT_DEFAULT_HEIGHT)
return width, height
def update_session_counters(self, idle=False):
"""
Hit this when the user enters a command in order to update idle timers
and command counters.
"""
# Store the timestamp of the user's last command.
self.cmd_last = time.time()
if not idle:
# Increment the user's command counter.
self.cmd_total += 1
# Player-visible idle time, not used in idle timeout calcs.
self.cmd_last_visible = time.time()
def data_in(self, text=None, **kwargs):
"""
Send User->Evennia. This will in effect
execute a command string on the server.
Especially handled keywords:
oob - this should hold a dictionary of oob command calls from
the oob-supporting protocol.
"""
#explicitly check for None since text can be an empty string, which is
#also valid
if text is not None:
# this is treated as a command input
#text = to_unicode(escape_control_sequences(text), encoding=self.encoding)
# handle the 'idle' command
if text.strip() == IDLE_COMMAND:
self.update_session_counters(idle=True)
return
if self.player:
# nick replacement
puppet = self.player.get_puppet(self.sessid)
if puppet:
text = puppet.nicks.nickreplace(text,
categories=("inputline", "channel"), include_player=True)
else:
text = self.player.nicks.nickreplace(text,
categories=("inputline", "channels"), include_player=False)
cmdhandler(self, text, callertype="session", sessid=self.sessid)
self.update_session_counters()
if "oob" in kwargs:
# handle oob instructions
global _OOB_HANDLER
if not _OOB_HANDLER:
from src.server.oobhandler import OOB_HANDLER as _OOB_HANDLER
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None))
#print "session.data_in: oobstruct:",oobstruct
for (funcname, args, kwargs) in oobstruct:
if funcname:
_OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs)
execute_cmd = data_in # alias
def data_out(self, text=None, **kwargs):
"""
Send Evennia -> User
"""
text = text if text else ""
if INLINEFUNC_ENABLED and not "raw" in kwargs:
text = parse_inlinefunc(text, strip="strip_inlinefunc" in kwargs, session=self)
self.sessionhandler.data_out(self, text=text, **kwargs)
def __eq__(self, other):
return self.address == other.address
def __str__(self):
"""
String representation of the user session class. We use
this a lot in the server logs.
"""
symbol = ""
if self.logged_in and hasattr(self, "player") and self.player:
symbol = "(#%s)" % self.player.id
try:
if hasattr(self.address, '__iter__'):
address = ":".join([str(part) for part in self.address])
else:
address = self.address
except Exception:
address = self.address
return "%s%s@%s" % (self.uname, symbol, address)
def __unicode__(self):
"""
Unicode representation
"""
return u"%s" % str(self)
# easy-access functions
#def login(self, player):
# "alias for at_login"
# self.session_login(player)
#def disconnect(self):
# "alias for session_disconnect"
# self.session_disconnect()
def msg(self, text='', **kwargs):
"alias for at_data_out"
self.data_out(text=text, **kwargs)
# Dummy API hooks for use during non-loggedin operation
def at_cmdset_get(self, **kwargs):
"dummy hook all objects with cmdsets need to have"
pass
# Mock db/ndb properties for allowing easy storage on the session
# (note that no databse is involved at all here. session.db.attr =
# value just saves a normal property in memory, just like ndb).
#@property
def ndb_get(self):
"""
A non-persistent store (ndb: NonDataBase). Everything stored
to this is guaranteed to be cleared when a server is shutdown.
Syntax is same as for the _get_db_holder() method and
property, e.g. obj.ndb.attr = value etc.
"""
try:
return self._ndb_holder
except AttributeError:
class NdbHolder(object):
"Holder for storing non-persistent attributes."
def all(self):
return [val for val in self.__dict__.keys()
if not val.startswith['_']]
def __getattribute__(self, key):
# return None if no matching attribute was found.
try:
return object.__getattribute__(self, key)
except AttributeError:
return None
self._ndb_holder = NdbHolder()
return self._ndb_holder
#@ndb.setter
def ndb_set(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to ndb object! "
string = "Use ndb.attr=value instead."
raise Exception(string)
#@ndb.deleter
def ndb_del(self):
"Stop accidental deletion."
raise Exception("Cannot delete the ndb object!")
ndb = property(ndb_get, ndb_set, ndb_del)
db = property(ndb_get, ndb_set, ndb_del)
# Mock access method for the session (there is no lock info
# at this stage, so we just present a uniform API)
def access(self, *args, **kwargs):
"Dummy method."
return True

129
lib/server/session.py Normal file
View file

@ -0,0 +1,129 @@
"""
This defines a generic session class. All connection instances (both
on Portal and Server side) should inherit from this class.
"""
import time
#------------------------------------------------------------
# Server Session
#------------------------------------------------------------
class Session(object):
"""
This class represents a player's session and is a template for
both portal- and server-side sessions.
Each connection will see two session instances created:
1) A Portal session. This is customized for the respective connection
protocols that Evennia supports, like Telnet, SSH etc. The Portal
session must call init_session() as part of its initialization. The
respective hook methods should be connected to the methods unique
for the respective protocol so that there is a unified interface
to Evennia.
2) A Server session. This is the same for all connected players,
regardless of how they connect.
The Portal and Server have their own respective sessionhandlers. These
are synced whenever new connections happen or the Server restarts etc,
which means much of the same information must be stored in both places
e.g. the portal can re-sync with the server when the server reboots.
"""
# names of attributes that should be affected by syncing.
_attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid',
'uname', 'logged_in', 'puid', 'encoding',
'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total',
'protocol_flags', 'server_data', "cmdset_storage_string")
def init_session(self, protocol_key, address, sessionhandler):
"""
Initialize the Session. This should be called by the protocol when
a new session is established.
protocol_key - telnet, ssh, ssl or web
address - client address
sessionhandler - reference to the sessionhandler instance
"""
# This is currently 'telnet', 'ssh', 'ssl' or 'web'
self.protocol_key = protocol_key
# Protocol address tied to this session
self.address = address
# suid is used by some protocols, it's a hex key.
self.suid = None
# unique id for this session
self.sessid = 0 # no sessid yet
# database id for the user connected to this session
self.uid = None
# user name, for easier tracking of sessions
self.uname = None
# if user has authenticated already or not
self.logged_in = False
# database id of puppeted object (if any)
self.puid = None
# session time statistics
self.conn_time = time.time()
self.cmd_last_visible = self.conn_time
self.cmd_last = self.conn_time
self.cmd_total = 0
self.encoding = "utf-8"
self.protocol_flags = {}
self.server_data = {}
# a back-reference to the relevant sessionhandler this
# session is stored in.
self.sessionhandler = sessionhandler
def get_sync_data(self):
"""
Return all data relevant to sync the session
"""
return dict((key, value) for key, value in self.__dict__.items()
if key in self._attrs_to_sync)
def load_sync_data(self, sessdata):
"""
Takes a session dictionary, as created by get_sync_data,
and loads it into the correct properties of the session.
"""
for propname, value in sessdata.items():
setattr(self, propname, value)
def at_sync(self):
"""
Called after a session has been fully synced (including
secondary operations such as setting self.player based
on uid etc).
"""
pass
# access hooks
def disconnect(self, reason=None):
"""
generic hook called from the outside to disconnect this session
should be connected to the protocols actual disconnect mechanism.
"""
pass
def data_out(self, text=None, **kwargs):
"""
generic hook for sending data out through the protocol. Server
protocols can use this right away. Portal sessions
should overload this to format/handle the outgoing data as needed.
"""
pass
def data_in(self, text=None, **kwargs):
"""
hook for protocols to send incoming data to the engine.
"""
pass

View file

@ -0,0 +1,492 @@
"""
This module defines handlers for storing sessions when handles
sessions of users connecting to the server.
There are two similar but separate stores of sessions:
ServerSessionHandler - this stores generic game sessions
for the game. These sessions has no knowledge about
how they are connected to the world.
PortalSessionHandler - this stores sessions created by
twisted protocols. These are dumb connectors that
handle network communication but holds no game info.
"""
import time
from django.conf import settings
from src.commands.cmdhandler import CMD_LOGINSTART
from src.utils.utils import variable_from_module, is_iter, \
to_str, to_unicode, strip_control_sequences
try:
import cPickle as pickle
except ImportError:
import pickle
# delayed imports
_PlayerDB = None
_ServerSession = None
_ServerConfig = None
_ScriptDB = None
# AMP signals
PCONN = chr(1) # portal session connect
PDISCONN = chr(2) # portal session disconnect
PSYNC = chr(3) # portal session sync
SLOGIN = chr(4) # server session login
SDISCONN = chr(5) # server session disconnect
SDISCONNALL = chr(6) # server session disconnect all
SSHUTD = chr(7) # server shutdown
SSYNC = chr(8) # server session sync
SCONN = chr(9) # server portal connection (for bots)
PCONNSYNC = chr(10) # portal post-syncing session
# i18n
from django.utils.translation import ugettext as _
SERVERNAME = settings.SERVERNAME
MULTISESSION_MODE = settings.MULTISESSION_MODE
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
def delayed_import():
"Helper method for delayed import of all needed entities"
global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
if not _ServerSession:
# we allow optional arbitrary serversession class for overloading
modulename, classname = settings.SERVER_SESSION_CLASS.rsplit(".", 1)
_ServerSession = variable_from_module(modulename, classname)
if not _PlayerDB:
from src.players.models import PlayerDB as _PlayerDB
if not _ServerConfig:
from src.server.models import ServerConfig as _ServerConfig
if not _ScriptDB:
from src.scripts.models import ScriptDB as _ScriptDB
# including once to avoid warnings in Python syntax checkers
_ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
#-----------------------------------------------------------
# SessionHandler base class
#------------------------------------------------------------
class SessionHandler(object):
"""
This handler holds a stack of sessions.
"""
def __init__(self):
"""
Init the handler.
"""
self.sessions = {}
def get_sessions(self, include_unloggedin=False):
"""
Returns the connected session objects.
"""
if include_unloggedin:
return self.sessions.values()
else:
return [session for session in self.sessions.values() if session.logged_in]
def get_session(self, sessid):
"""
Get session by sessid
"""
return self.sessions.get(sessid, None)
def get_all_sync_data(self):
"""
Create a dictionary of sessdata dicts representing all
sessions in store.
"""
return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items())
def oobstruct_parser(self, oobstruct):
"""
Helper method for each session to use to parse oob structures
(The 'oob' kwarg of the msg() method).
Allowed input oob structures are:
cmdname
((cmdname,), (cmdname,))
(cmdname,(arg, ))
(cmdname,(arg1,arg2))
(cmdname,{key:val,key2:val2})
(cmdname, (args,), {kwargs})
((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,)))
outputs an ordered structure on the form
((cmdname, (args,), {kwargs}), ...), where the two last
parts of each tuple may be empty
"""
def _parse(oobstruct):
slen = len(oobstruct)
if not oobstruct:
return tuple(None, (), {})
elif not hasattr(oobstruct, "__iter__"):
# a singular command name, without arguments or kwargs
return (oobstruct.lower(), (), {})
# regardless of number of args/kwargs, the first element must be
# the function name. We will not catch this error if not, but
# allow it to propagate.
if slen == 1:
return (oobstruct[0].lower(), (), {})
elif slen == 2:
if isinstance(oobstruct[1], dict):
# cmdname, {kwargs}
return (oobstruct[0].lower(), (), dict(oobstruct[1]))
elif isinstance(oobstruct[1], (tuple, list)):
# cmdname, (args,)
return (oobstruct[0].lower(), list(oobstruct[1]), {})
else:
# cmdname, cmdname
return ((oobstruct[0].lower(), (), {}), (oobstruct[1].lower(), (), {}))
else:
# cmdname, (args,), {kwargs}
return (oobstruct[0].lower(), list(oobstruct[1]), dict(oobstruct[2]))
if hasattr(oobstruct, "__iter__"):
# differentiate between (cmdname, cmdname),
# (cmdname, (args), {kwargs}) and ((cmdname,(args),{kwargs}),
# (cmdname,(args),{kwargs}), ...)
if oobstruct and isinstance(oobstruct[0], basestring):
return (list(_parse(oobstruct)),)
else:
out = []
for oobpart in oobstruct:
out.append(_parse(oobpart))
return (list(out),)
return (_parse(oobstruct),)
#------------------------------------------------------------
# Server-SessionHandler class
#------------------------------------------------------------
class ServerSessionHandler(SessionHandler):
"""
This object holds the stack of sessions active in the game at
any time.
A session register with the handler in two steps, first by
registering itself with the connect() method. This indicates an
non-authenticated session. Whenever the session is authenticated
the session together with the related player is sent to the login()
method.
"""
# AMP communication methods
def __init__(self):
"""
Init the handler.
"""
self.sessions = {}
self.server = None
self.server_data = {"servername": SERVERNAME}
def portal_connect(self, portalsession):
"""
Called by Portal when a new session has connected.
Creates a new, unlogged-in game session.
portalsession is a dictionary of all property:value keys
defining the session and which is marked to
be synced.
"""
delayed_import()
global _ServerSession, _PlayerDB, _ScriptDB
sess = _ServerSession()
sess.sessionhandler = self
sess.load_sync_data(portalsession)
if sess.logged_in and sess.uid:
# this can happen in the case of auto-authenticating
# protocols like SSH
sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid)
sess.at_sync()
# validate all scripts
_ScriptDB.objects.validate()
self.sessions[sess.sessid] = sess
sess.data_in(CMD_LOGINSTART)
def portal_session_sync(self, portalsessiondata):
"""
Called by Portal when it wants to update a single session (e.g.
because of all negotiation protocols have finally replied)
"""
sessid = portalsessiondata.get("sessid")
session = self.sessions.get(sessid)
if session:
# since some of the session properties may have had
# a chance to change already before the portal gets here
# the portal doesn't send all sessiondata here, but only
# ones which should only be changed from portal (like
# protocol_flags etc)
session.load_sync_data(portalsessiondata)
def portal_disconnect(self, sessid):
"""
Called by Portal when portal reports a closing of a session
from the portal side.
"""
session = self.sessions.get(sessid, None)
if not session:
return
player = session.player
if player:
nsess = len(self.sessions_from_player(player)) - 1
remaintext = nsess and "%i session%s remaining" % (nsess, nsess > 1 and "s" or "") or "no more sessions"
session.log(_('Connection dropped: %s %s (%s)' % (session.player, session.address, remaintext)))
session.at_disconnect()
session.disconnect()
del self.sessions[session.sessid]
def portal_sessions_sync(self, portalsessions):
"""
Syncing all session ids of the portal with the ones of the
server. This is instantiated by the portal when reconnecting.
portalsessions is a dictionary {sessid: {property:value},...} defining
each session and the properties in it which should
be synced.
"""
delayed_import()
global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
for sess in self.sessions.values():
# we delete the old session to make sure to catch eventual
# lingering references.
del sess
for sessid, sessdict in portalsessions.items():
sess = _ServerSession()
sess.sessionhandler = self
sess.load_sync_data(sessdict)
if sess.uid:
sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid)
self.sessions[sessid] = sess
sess.at_sync()
# after sync is complete we force-validate all scripts
# (this also starts them)
init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None)
_ScriptDB.objects.validate(init_mode=init_mode)
_ServerConfig.objects.conf("server_restart_mode", delete=True)
# announce the reconnection
self.announce_all(_(" ... Server restarted."))
# server-side access methods
def start_bot_session(self, protocol_path, configdict):
"""
This method allows the server-side to force the Portal to create
a new bot session using the protocol specified by protocol_path,
which should be the full python path to the class, including the
class name, like "src.server.portal.irc.IRCClient".
The new session will use the supplied player-bot uid to
initiate an already logged-in connection. The Portal will
treat this as a normal connection and henceforth so will the
Server.
"""
data = {"protocol_path":protocol_path,
"config":configdict}
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation=SCONN,
data=data)
def portal_shutdown(self):
"""
Called by server when shutting down the portal.
"""
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation=SSHUTD,
data="")
def login(self, session, player, testmode=False):
"""
Log in the previously unloggedin session and the player we by
now should know is connected to it. After this point we
assume the session to be logged in one way or another.
testmode - this is used by unittesting for faking login without
any AMP being actually active
"""
# we have to check this first before uid has been assigned
# this session.
if not self.sessions_from_player(player):
player.is_connected = True
# sets up and assigns all properties on the session
session.at_login(player)
# player init
player.at_init()
# Check if this is the first time the *player* logs in
if player.db.FIRST_LOGIN:
player.at_first_login()
del player.db.FIRST_LOGIN
player.at_pre_login()
if MULTISESSION_MODE == 0:
# disconnect all previous sessions.
self.disconnect_duplicate_sessions(session)
nsess = len(self.sessions_from_player(player))
totalstring = "%i session%s total" % (nsess, nsess > 1 and "s" or "")
session.log(_('Logged in: %s %s (%s)' % (player, session.address, totalstring)))
session.logged_in = True
# sync the portal to the session
sessdata = {"logged_in": True}
if not testmode:
self.server.amp_protocol.call_remote_PortalAdmin(session.sessid,
operation=SLOGIN,
data=sessdata)
player.at_post_login(sessid=session.sessid)
def disconnect(self, session, reason=""):
"""
Called from server side to remove session and inform portal
of this fact.
"""
session = self.sessions.get(session.sessid)
if not session:
return
if hasattr(session, "player") and session.player:
# only log accounts logging off
nsess = len(self.sessions_from_player(session.player)) - 1
remaintext = nsess and "%i session%s remaining" % (nsess, nsess > 1 and "s" or "") or "no more sessions"
session.log(_('Logged out: %s %s (%s)' % (session.player, session.address, remaintext)))
session.at_disconnect()
sessid = session.sessid
del self.sessions[sessid]
# inform portal that session should be closed.
self.server.amp_protocol.call_remote_PortalAdmin(sessid,
operation=SDISCONN,
data=reason)
def all_sessions_portal_sync(self):
"""
This is called by the server when it reboots. It syncs all session data
to the portal. Returns a deferred!
"""
sessdata = self.get_all_sync_data()
return self.server.amp_protocol.call_remote_PortalAdmin(0,
operation=SSYNC,
data=sessdata)
def disconnect_all_sessions(self, reason=_("You have been disconnected.")):
"""
Cleanly disconnect all of the connected sessions.
"""
for session in self.sessions:
del session
# tell portal to disconnect all sessions
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation=SDISCONNALL,
data=reason)
def disconnect_duplicate_sessions(self, curr_session,
reason=_("Logged in from elsewhere. Disconnecting.")):
"""
Disconnects any existing sessions with the same user.
"""
uid = curr_session.uid
doublet_sessions = [sess for sess in self.sessions.values()
if sess.logged_in
and sess.uid == uid
and sess != curr_session]
for session in doublet_sessions:
self.disconnect(session, reason)
def validate_sessions(self):
"""
Check all currently connected sessions (logged in and not)
and see if any are dead or idle
"""
tcurr = time.time()
reason = _("Idle timeout exceeded, disconnecting.")
for session in (session for session in self.sessions.values()
if session.logged_in and IDLE_TIMEOUT > 0
and (tcurr - session.cmd_last) > IDLE_TIMEOUT):
self.disconnect(session, reason=reason)
def player_count(self):
"""
Get the number of connected players (not sessions since a
player may have more than one session depending on settings).
Only logged-in players are counted here.
"""
return len(set(session.uid for session in self.sessions.values() if session.logged_in))
def session_from_sessid(self, sessid):
"""
Return session based on sessid, or None if not found
"""
if is_iter(sessid):
return [self.sessions.get(sid) for sid in sessid if sid in self.sessions]
return self.sessions.get(sessid)
def session_from_player(self, player, sessid):
"""
Given a player and a session id, return the actual session object
"""
if is_iter(sessid):
sessions = [self.sessions.get(sid) for sid in sessid]
s = [sess for sess in sessions if sess and sess.logged_in and player.uid == sess.uid]
return s
session = self.sessions.get(sessid)
return session and session.logged_in and player.uid == session.uid and session or None
def sessions_from_player(self, player):
"""
Given a player, return all matching sessions.
"""
uid = player.uid
return [session for session in self.sessions.values() if session.logged_in and session.uid == uid]
def sessions_from_character(self, character):
"""
Given a game character, return any matching sessions.
"""
sessid = character.sessid.get()
if is_iter(sessid):
return [self.sessions.get(sess) for sess in sessid if sessid in self.sessions]
return self.sessions.get(sessid)
def announce_all(self, message):
"""
Send message to all connected sessions
"""
for sess in self.sessions.values():
self.data_out(sess, message)
def data_out(self, session, text="", **kwargs):
"""
Sending data Server -> Portal
"""
text = text and to_str(to_unicode(text), encoding=session.encoding)
self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid,
msg=text,
data=kwargs)
def data_in(self, sessid, text="", **kwargs):
"""
Data Portal -> Server
"""
session = self.sessions.get(sessid, None)
if session:
text = text and to_unicode(strip_control_sequences(text), encoding=session.encoding)
session.data_in(text=text, **kwargs)
SESSIONS = ServerSessionHandler()

76
lib/server/tests.py Normal file
View file

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""
Unit testing of the 'objects' Evennia component.
Runs as part of the Evennia's test suite with 'manage.py test"
Please add new tests to this module as needed.
Guidelines:
A 'test case' is testing a specific component and is defined as a class
inheriting from unittest.TestCase. The test case class can have a method
setUp() that creates and sets up the testing environment.
All methods inside the test case class whose names start with 'test' are
used as test methods by the runner. Inside the test methods, special member
methods assert*() are used to test the behaviour.
"""
import sys
import glob
try:
from django.utils.unittest import TestCase
except ImportError:
from django.test import TestCase
try:
from django.utils import unittest
except ImportError:
import unittest
from django.conf import settings
from django.test.simple import DjangoTestSuiteRunner
from src.utils.utils import mod_import
class EvenniaTestSuiteRunner(DjangoTestSuiteRunner):
"""
This test runner only runs tests on the apps specified in src/ and game/ to
avoid running the large number of tests defined by Django
"""
def build_suite(self, test_labels, extra_tests=None, **kwargs):
"""
Build a test suite for Evennia. test_labels is a list of apps to test.
If not given, a subset of settings.INSTALLED_APPS will be used.
"""
if not test_labels:
test_labels = [applabel.rsplit('.', 1)[1] for applabel in settings.INSTALLED_APPS
if (applabel.startswith('src.') or applabel.startswith('game.'))]
return super(EvenniaTestSuiteRunner, self).build_suite(test_labels, extra_tests=extra_tests, **kwargs)
def suite():
"""
This function is called automatically by the django test runner.
This also collates tests from packages that are not formally django applications.
"""
from src.locks import tests as locktests
from src.utils import tests as utiltests
from src.commands.default import tests as commandtests
tsuite = unittest.TestSuite()
tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]))
# test modules from non-django apps
tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(commandtests))
tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(locktests))
tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(utiltests))
for path in glob.glob("../src/tests/test_*.py"):
testmod = mod_import(path)
tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(testmod))
#from src.tests import test_commands_cmdhandler
#tsuite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_commands_cmdhandler))
return tsuite

134
lib/server/webserver.py Normal file
View file

@ -0,0 +1,134 @@
"""
This implements resources for twisted webservers using the wsgi
interface of django. This alleviates the need of running e.g. an
apache server to serve Evennia's web presence (although you could do
that too if desired).
The actual servers are started inside server.py as part of the Evennia
application.
(Lots of thanks to http://githup.com/clemensha/twisted-wsgi-django for
a great example/aid on how to do this.)
"""
import urlparse
from urllib import quote as urlquote
from twisted.web import resource, http
from twisted.internet import reactor
from twisted.application import internet
from twisted.web.proxy import ReverseProxyResource
from twisted.web.server import NOT_DONE_YET
from twisted.web.wsgi import WSGIResource
from django.core.handlers.wsgi import WSGIHandler
from settings import UPSTREAM_IPS
#
# X-Forwarded-For Handler
#
class HTTPChannelWithXForwardedFor(http.HTTPChannel):
def allHeadersReceived(self):
"""
Check to see if this is a reverse proxied connection.
"""
CLIENT = 0
http.HTTPChannel.allHeadersReceived(self)
req = self.requests[-1]
client_ip, port = self.transport.client
proxy_chain = req.getHeader('X-FORWARDED-FOR')
if proxy_chain and client_ip in UPSTREAM_IPS:
forwarded = proxy_chain.split(', ', 1)[CLIENT]
self.transport.client = (forwarded, port)
# Monkey-patch Twisted to handle X-Forwarded-For.
http.HTTPFactory.protocol = HTTPChannelWithXForwardedFor
class EvenniaReverseProxyResource(ReverseProxyResource):
def getChild(self, path, request):
"""
Create and return a proxy resource with the same proxy configuration
as this one, except that its path also contains the segment given by
C{path} at the end.
"""
return EvenniaReverseProxyResource(
self.host, self.port, self.path + '/' + urlquote(path, safe=""),
self.reactor)
def render(self, request):
"""
Render a request by forwarding it to the proxied server.
"""
# RFC 2616 tells us that we can omit the port if it's the default port,
# but we have to provide it otherwise
request.content.seek(0, 0)
qs = urlparse.urlparse(request.uri)[4]
if qs:
rest = self.path + '?' + qs
else:
rest = self.path
clientFactory = self.proxyClientFactoryClass(
request.method, rest, request.clientproto,
request.getAllHeaders(), request.content.read(), request)
self.reactor.connectTCP(self.host, self.port, clientFactory)
return NOT_DONE_YET
#
# Website server resource
#
class DjangoWebRoot(resource.Resource):
"""
This creates a web root (/) that Django
understands by tweaking the way the
child instancee are recognized.
"""
def __init__(self, pool):
"""
Setup the django+twisted resource
"""
resource.Resource.__init__(self)
self.wsgi_resource = WSGIResource(reactor, pool, WSGIHandler())
def getChild(self, path, request):
"""
To make things work we nudge the
url tree to make this the root.
"""
path0 = request.prepath.pop(0)
request.postpath.insert(0, path0)
return self.wsgi_resource
#
# Threaded Webserver
#
class WSGIWebServer(internet.TCPServer):
"""
This is a WSGI webserver. It makes sure to start
the threadpool after the service itself started,
so as to register correctly with the twisted daemon.
call with WSGIWebServer(threadpool, port, wsgi_resource)
"""
def __init__(self, pool, *args, **kwargs):
"This just stores the threadpool"
self.pool = pool
internet.TCPServer.__init__(self, *args, **kwargs)
def startService(self):
"Start the pool after the service"
internet.TCPServer.startService(self)
self.pool.start()
def stopService(self):
"Safely stop the pool after service stop."
internet.TCPServer.stopService(self)
self.pool.stop()