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