Trunk: Merged griatch-branch. This implements a new reload mechanism - splitting Evennia into two processes: Server and Portal with different tasks. Also cleans and fixes several bugs in script systems as well as introduces i18n (courtesy of raydeejay).

This commit is contained in:
Griatch 2011-09-03 10:22:19 +00:00
parent 14dae44a46
commit f13e8cdf7c
50 changed files with 3175 additions and 2565 deletions

394
src/server/amp.py Normal file
View file

@ -0,0 +1,394 @@
"""
Contains the protocols, commands, and client factory needed for the server
to service the MUD portal proxy.
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
"""
import os
try:
import cPickle as pickle
except ImportError:
import pickle
from twisted.protocols import amp
from twisted.internet import protocol, defer, reactor
from django.conf import settings
from src.utils import utils
from src.server.models import ServerConfig
from src.scripts.models import ScriptDB
from src.players.models import PlayerDB
from src.server.serversession import ServerSession
PORTAL_RESTART = os.path.join(settings.GAME_DIR, "portal.restart")
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart")
# i18n
from django.utils.translation import ugettext as _
# Signals
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 new AMPProtocol protocol instances to use for accepting
connections from TCPServer.
"""
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 new AMPProtocol protocol instances to use to connect
to the MUD server. It also maintains the portal attribute
on the ProxyService instance, which is used for piping input
from Telnet to the MUD server.
"""
# 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 not get_restart_mode(SERVER_RESTART):
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.
"""
self.portal.sessions.announce_all(" ...")
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
class MsgPortal2Server(amp.Command):
"""
Message portal -> server
"""
arguments = [('sessid', amp.Integer()),
('msg', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
class MsgServer2Portal(amp.Command):
"""
Message server -> portal
"""
arguments = [('sessid', 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
"""
arguments = [('sessid', 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.
"""
arguments = [('sessid', amp.Integer()),
('operation', amp.String()),
('data', amp.String())]
errors = [(Exception, 'EXCEPTION')]
response = []
#------------------------------------------------------------
# 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. It is called on both sides,
so we need to make sure to only trigger resync from the
server side.
"""
if hasattr(self.factory, "portal"):
sessdata = self.factory.portal.sessions.get_all_sync_data()
self.call_remote_ServerAdmin(0,
"PSYNC",
data=sessdata)
if get_restart_mode(SERVER_RESTART):
msg = _(" ... Server restarted.")
self.factory.portal.sessions.announce_all(msg)
# Error handling
def errback(self, e, info):
"error handler, to avoid dropping connections on server tracebacks."
e.trap(Exception)
print _("AMP Error for %(info)s: %(e)s") % {'info': info, 'e': e.getErrorMessage()}
# Message definition + helper methods to call/create each message type
# Portal -> Server Msg
def amp_msg_portal2server(self, sessid, msg, data):
"""
Relays message to server. This method is executed on the Server.
"""
#print "msg portal -> server (server side):", sessid, msg
self.factory.server.sessions.data_in(sessid, msg, pickle.loads(utils.to_str(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
self.callRemote(MsgPortal2Server,
sessid=sessid,
msg=msg,
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgPortal2Server")
# Server -> Portal message
def amp_msg_server2portal(self, sessid, msg, data):
"""
Relays message to Portal. This method is executed on the Portal.
"""
#print "msg server->portal (portal side):", sessid, msg
self.factory.portal.sessions.data_out(sessid, msg, pickle.loads(utils.to_str(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
self.callRemote(MsgServer2Portal,
sessid=sessid,
msg=utils.to_str(msg),
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgServer2Portal")
# Server administration from the Portal side
def amp_server_admin(self, sessid, operation, data):
"""
This allows the portal to perform admin
operations on the server. This is executed on the Server.
"""
data = pickle.loads(utils.to_str(data))
#print "serveradmin (server side):", sessid, operation, data
if operation == 'PCONN': #portal_session_connect
# create a new, session and sync it
sess = ServerSession()
sess.sessionhandler = self.factory.server.sessions
sess.load_sync_data(data)
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() # this runs initialization without acr
self.factory.server.sessions.portal_connect(sessid, sess)
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.
sesslist = []
server_sessionhandler = self.factory.server.sessions
for sessid, sessdict in data.items():
sess = ServerSession()
sess.sessionhandler = server_sessionhandler
sess.load_sync_data(sessdict)
if sess.uid:
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid)
sess.at_sync()
sesslist.append(sess)
# replace sessions on server
server_sessionhandler.portal_session_sync(sesslist)
# after sync is complete we force-validate all scripts (this starts everthing)
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)
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, operation, data
data = utils.to_str(pickle.dumps(data))
self.callRemote(ServerAdmin,
sessid=sessid,
operation=operation,
data=data).addErrback(self.errback, "ServerAdmin")
# Portal administraton from the Server side
def amp_portal_admin(self, sessid, operation, data):
"""
This allows the server to perform admin
operations on the portal. This is executed on the Portal.
"""
data = pickle.loads(utils.to_str(data))
#print "portaladmin (portal side):", sessid, operation, data
if operation == 'SLOGIN': # 'server_session_login'
# a session has authenticated; sync it.
sess = self.factory.portal.sessions.get_session(sessid)
sess.load_sync_data(data)
elif operation == 'SDISCONN': #'server_session_disconnect'
# the server is ordering to disconnect the session
self.factory.portal.sessions.server_disconnect(sessid, reason=data)
elif operation == 'SDISCONNALL': #'server_session_disconnect_all'
# server orders all sessions to disconnect
self.factory.portal.sessions.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. We don't overwrite any sessions,
# just update data on them and remove eventual ones that are
# out of sync (shouldn't happen normally).
portal_sessionhandler = self.factory.portal.sessions.sessions
to_save = [sessid for sessid in data if sessid in portal_sessionhandler.sessions]
to_delete = [sessid for sessid in data if sessid not in to_save]
# save protocols
for sessid in to_save:
portal_sessionhandler.sessions[sessid].load_sync_data(data[sessid])
# disconnect missing protocols
for sessid in to_delete:
portal_sessionhandler.server_disconnect(sessid)
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.
"""
#print "portaladmin (server side):", sessid, operation, data
data = utils.to_str(pickle.dumps(data))
self.callRemote(PortalAdmin,
sessid=sessid,
operation=operation,
data=data).addErrback(self.errback, "PortalAdmin")

View file

@ -13,6 +13,9 @@ from src.server.models import ServerConfig
from src.help.models import HelpEntry
from src.utils import create
# i18n
from django.utils.translation import ugettext as _
def create_config_values():
"""
Creates the initial config values.
@ -31,7 +34,7 @@ def create_objects():
Creates the #1 player and Limbo room.
"""
print " Creating objects (Player #1 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.
@ -55,7 +58,7 @@ def create_objects():
typeclass=character_typeclass,
user=god_user)
god_character.id = 1
god_character.db.desc = 'This is User #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.save()
@ -63,12 +66,13 @@ def create_objects():
# Limbo is the initial starting room.
room_typeclass = settings.BASE_ROOM_TYPECLASS
limbo_obj = create.create_object(room_typeclass, 'Limbo')
limbo_obj = create.create_object(room_typeclass, _('Limbo'))
limbo_obj.id = 2
string = "Welcome to your new %chEvennia%cn-based game."
string = " Welcome to your new {wEvennia{n-based game."
string += " From here you are ready to begin development."
string += " If you should need help or would like to participate"
string += " in community discussions, visit http://evennia.com."
string = _(string)
limbo_obj.db.desc = string
limbo_obj.save()
@ -80,7 +84,7 @@ def create_channels():
"""
Creates some sensible default channels.
"""
print " Creating default channels ..."
print _(" Creating default channels ...")
# public channel
key, aliases, desc, locks = settings.CHANNEL_PUBLIC
@ -103,12 +107,12 @@ def import_MUX_help_files():
"""
Imports the MUX help files.
"""
print " Importing MUX help database (devel reference only) ..."
print _(" Importing MUX help database (devel reference only) ...")
management.call_command('loaddata', '../src/help/mux_help_db.json', verbosity=0)
# categorize the MUX help files into its own category.
default_category = "MUX"
print " Moving imported help db to help category '%s'." \
% default_category
print _(" Moving imported help db to help category '%(default)s'." \
% {'default': default_category})
HelpEntry.objects.all_to_category(default_category)
def create_system_scripts():
@ -118,7 +122,7 @@ def create_system_scripts():
"""
from src.scripts import scripts
print " Creating and starting global scripts ..."
print _(" Creating and starting global scripts ...")
# check so that all sessions are alive.
script1 = create.create_script(scripts.CheckSessions)
@ -127,7 +131,7 @@ def create_system_scripts():
# update the channel handler to make sure it's in sync
script3 = create.create_script(scripts.ValidateChannelHandler)
if not script1 or not script2 or not script3:
print " Error creating system scripts."
print _(" Error creating system scripts.")
def start_game_time():
"""
@ -136,7 +140,7 @@ def start_game_time():
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 ..."
print _(" Starting in-game time ...")
from src.utils import gametime
gametime.init_gametime()
@ -155,17 +159,17 @@ def create_admin_media_links():
dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
apath = os.path.join(settings.ADMIN_MEDIA_ROOT)
if os.path.isdir(apath):
print " ADMIN_MEDIA_ROOT already exists. Ignored."
print _(" ADMIN_MEDIA_ROOT already exists. Ignored.")
return
if os.name == 'nt':
print " Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode)."
print _(" Admin-media files copied to ADMIN_MEDIA_ROOT (Windows mode).")
os.mkdir(apath)
os.system('xcopy "%s" "%s" /e /q /c' % (dpath, apath))
if os.name == 'posix':
os.symlink(dpath, apath)
print " Admin-media symlinked to ADMIN_MEDIA_ROOT."
print _(" Admin-media symlinked to ADMIN_MEDIA_ROOT.")
else:
print " Admin-media files should be copied manually to ADMIN_MEDIA_ROOT."
print _(" Admin-media files should be copied manually to ADMIN_MEDIA_ROOT.")
def handle_setup(last_step):
"""

View file

@ -18,6 +18,9 @@ from src.utils.idmapper.models import SharedMemoryModel
from src.utils import logger, utils
from src.server.manager import ServerConfigManager
# i18n
from django.utils.translation import ugettext as _
#------------------------------------------------------------
#
# ServerConfig
@ -82,7 +85,7 @@ class ServerConfig(SharedMemoryModel):
"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)
logger.log_errmsg(_("ServerConfig cannot store db objects! (%s)" % value))
return
self.db_value = pickle.dumps(value)
self.save()

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

@ -0,0 +1,304 @@
"""
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.application import internet, service
from twisted.internet import protocol, reactor
from twisted.web import server, static
from django.conf import settings
from src.utils.utils import get_evennia_version
from src.server.sessionhandler import PORTAL_SESSIONS
if os.name == 'nt':
# For Windows we need to handle pid files manually.
PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, 'portal.pid')
# i18n
from django.utils.translation import ugettext as _
#------------------------------------------------------------
# Evennia Portal settings
#------------------------------------------------------------
VERSION = get_evennia_version()
SERVERNAME = settings.SERVERNAME
PORTAL_RESTART = os.path.join(settings.GAME_DIR, 'portal.restart')
TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
TELNET_INTERFACES = settings.TELNET_INTERFACES
SSL_INTERFACES = settings.SSL_INTERFACES
SSH_INTERFACES = settings.SSH_INTERFACES
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
IMC2_ENABLED = settings.IMC2_ENABLED
IRC_ENABLED = settings.IRC_ENABLED
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
AMP_ENABLED = AMP_HOST and AMP_PORT
#------------------------------------------------------------
# Portal Service object
#------------------------------------------------------------
class Portal(object):
"""
The main Portal server handler. This object sets up the database and
tracks and interlinks all the twisted network services that make up
Portal.
"""
def __init__(self, application):
"""
Setup the server.
application - an instantiated Twisted application
"""
sys.path.append('.')
# create a store of services
self.services = service.IServiceCollection(application)
self.amp_protocol = None # set by amp factory
self.sessions = PORTAL_SESSIONS
self.sessions.portal = self
print '\n' + '-'*50
# Make info output to the terminal.
self.terminal_output()
print '-'*50
# set a callback if the server is killed abruptly,
# by Ctrl-C, reboot etc.
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _abrupt=True)
self.game_running = False
def terminal_output(self):
"""
Outputs server startup info to the terminal.
"""
print _(' %(servername)s Portal (%(version)s) started.') % {'servername': SERVERNAME, 'version': VERSION}
if AMP_ENABLED:
print " amp (Server): %s" % AMP_PORT
if TELNET_ENABLED:
ports = ", ".join([str(port) for port in TELNET_PORTS])
ifaces = ",".join([" %s" % iface for iface in TELNET_INTERFACES if iface != '0.0.0.0'])
print " telnet%s: %s" % (ifaces, ports)
if SSH_ENABLED:
ports = ", ".join([str(port) for port in SSH_PORTS])
ifaces = ",".join([" %s" % iface for iface in SSH_INTERFACES if iface != '0.0.0.0'])
print " ssh%s: %s" % (ifaces, ports)
if SSL_ENABLED:
ports = ", ".join([str(port) for port in SSL_PORTS])
ifaces = ",".join([" %s" % iface for iface in SSL_INTERFACES if iface != '0.0.0.0'])
print " ssl%s: %s" % (ifaces, ports)
if WEBSERVER_ENABLED:
clientstring = ""
if WEBCLIENT_ENABLED:
clientstring = '/client'
ports = ", ".join([str(port) for port in WEBSERVER_PORTS])
ifaces = ",".join([" %s" % iface for iface in WEBSERVER_INTERFACES if iface != '0.0.0.0'])
print " webserver%s%s: %s" % (clientstring, ifaces, ports)
def set_restart_mode(self, mode=None):
"""
This manages the flag file that tells the runner if the server should
be restarted or is shutting down. Valid modes are True/False and None.
If mode is None, no change will be done to the flag file.
"""
if mode == None:
return
f = open(PORTAL_RESTART, 'w')
print _("writing mode=%(mode)s to %(portal_restart)s") % {'mode': mode, 'portal_restart': PORTAL_RESTART}
f.write(str(mode))
f.close()
def shutdown(self, restart=None, _abrupt=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.
_abrupt - this is set if server is stopped by a kill command,
in which case the reactor is dead anyway.
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.
"""
self.set_restart_mode(restart)
if not _abrupt:
reactor.callLater(0, reactor.stop)
if os.name == 'nt' and os.path.exists(PORTAL_PIDFILE):
# for Windows we need to remove pid files manually
os.remove(PORTAL_PIDFILE)
#------------------------------------------------------------
#
# 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)
if AMP_ENABLED:
# The AMP protocol handles the communication between
# the portal and the mud server. Only reason to ever deactivate
# it would be during testing and debugging.
from src.server import amp
factory = amp.AmpClientFactory(PORTAL)
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
amp_client.setName('evennia_amp')
PORTAL.services.addService(amp_client)
# We group all the various services under the same twisted app.
# These will gradually be started as they are initialized below.
if TELNET_ENABLED:
# Start telnet game connections
from src.server import telnet
for interface in TELNET_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(TELNET_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in TELNET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.protocol = telnet.TelnetProtocol
factory.sessionhandler = PORTAL_SESSIONS
telnet_service = internet.TCPServer(port, factory, interface=interface)
telnet_service.setName('EvenniaTelnet%s' % pstring)
PORTAL.services.addService(telnet_service)
if SSL_ENABLED:
# Start SSL game connection (requires PyOpenSSL).
from src.server import ssl
for interface in SSL_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(SSL_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSL_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.sessionhandler = PORTAL_SESSIONS
factory.protocol = ssl.SSLProtocol
ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface)
ssl_service.setName('EvenniaSSL%s' % pstring)
PORTAL.services.addService(ssl_service)
if SSH_ENABLED:
# Start SSH game connections. Will create a keypair in evennia/game if necessary.
from src.server import ssh
for interface in SSH_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(SSH_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
'protocolArgs':(),
'sessions':PORTAL_SESSIONS})
ssh_service = internet.TCPServer(port, factory, interface=interface)
ssh_service.setName('EvenniaSSH%s' % pstring)
PORTAL.services.addService(ssh_service)
if WEBSERVER_ENABLED:
# Start a django-compatible webserver.
from twisted.python import threadpool
from src.server.webserver import DjangoWebRoot, WSGIWebServer
# start a thread pool and define the root url (/) as a wsgi resource
# recognized by Django
threads = threadpool.ThreadPool()
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
from src.server.webclient import WebClient
webclient = WebClient()
webclient.sessionhandler = PORTAL_SESSIONS
web_root.putChild("webclientdata", webclient)
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
for interface in WEBSERVER_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(WEBSERVER_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in WEBSERVER_PORTS:
pstring = "%s:%s" % (ifacestr, port)
# create the webserver
webserver = WSGIWebServer(threads, port, web_site, interface=interface)
webserver.setName('EvenniaWebServer%s' % pstring)
PORTAL.services.addService(webserver)
if IRC_ENABLED:
# IRC channel connections
from src.comms import irc
irc.connect_all()
if IMC2_ENABLED:
# IMC2 channel connections
from src.comms import imc2
imc2.connect_all()
if os.name == 'nt':
# Windows only: Set PID file manually
f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w')
f.write(str(os.getpid()))
f.close()

View file

@ -1,8 +1,6 @@
"""
This module implements the main Evennia server process, the core of
the game engine. Don't import this module directly! If you need to
access the server processes from code, instead go via the session-
handler: src.sessionhandler.SESSIONS.server
the game engine.
This module should be started with the 'twistd' executable since it
sets up all the networking features. (this is done automatically
@ -12,6 +10,7 @@ by game/evennia.py).
import time
import sys
import os
import signal
if os.name == 'nt':
# For Windows batchfile we need an extra path insertion here.
sys.path.insert(0, os.path.dirname(os.path.dirname(
@ -22,14 +21,23 @@ from twisted.internet import protocol, reactor, defer
from twisted.web import server, static
from django.db import connection
from django.conf import settings
from src.scripts.models import ScriptDB
from src.server.models import ServerConfig
from src.server.sessionhandler import SESSIONS
from src.server import initial_setup
from src.utils.utils import get_evennia_version
from src.comms import channelhandler
from src.server.sessionhandler import SESSIONS
if os.name == 'nt':
# For Windows we need to handle pid files manually.
SERVER_PIDFILE = os.path.join(settings.GAME_DIR, 'server.pid')
SERVER_RESTART = os.path.join(settings.GAME_DIR, 'server.restart')
# i18n
from django.utils.translation import ugettext as _
#------------------------------------------------------------
# Evennia Server settings
@ -38,23 +46,10 @@ from src.comms import channelhandler
SERVERNAME = settings.SERVERNAME
VERSION = get_evennia_version()
TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
AMP_ENABLED = True
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
TELNET_INTERFACES = settings.TELNET_INTERFACES
SSL_INTERFACES = settings.SSL_INTERFACES
SSH_INTERFACES = settings.SSH_INTERFACES
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
IMC2_ENABLED = settings.IMC2_ENABLED
IRC_ENABLED = settings.IRC_ENABLED
#------------------------------------------------------------
# Evennia Main Server object
@ -75,10 +70,13 @@ class Evennia(object):
"""
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
print '\n' + '-'*50
# Database-specific startup optimizations.
@ -87,20 +85,11 @@ class Evennia(object):
# Run the initial setup if needed
self.run_initial_setup()
# we have to null this here.
SESSIONS.session_count(0)
# we link ourself to the sessionhandler so other modules don't have to
# re-import the server module itself (which would re-initialize it).
SESSIONS.server = self
self.start_time = time.time()
# initialize channelhandler
channelhandler.CHANNELHANDLER.update()
# init all global scripts
ScriptDB.objects.validate(init_mode=True)
# Make info output to the terminal.
self.terminal_output()
@ -138,7 +127,7 @@ class Evennia(object):
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.'
print _(' Server started for the first time. Setting defaults.')
initial_setup.handle_setup(0)
print '-'*50
elif int(last_initial_setup_step) >= 0:
@ -146,51 +135,86 @@ class Evennia(object):
# 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 %s.' % \
last_initial_setup_step
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 terminal_output(self):
"""
Outputs server startup info to the terminal.
"""
print ' %s (%s) started on port(s):' % (SERVERNAME, VERSION)
if TELNET_ENABLED:
ports = ", ".join([str(port) for port in TELNET_PORTS])
ifaces = ",".join([" %s" % iface for iface in TELNET_INTERFACES if iface != '0.0.0.0'])
print " telnet%s: %s" % (ifaces, ports)
if SSH_ENABLED:
ports = ", ".join([str(port) for port in SSH_PORTS])
ifaces = ",".join([" %s" % iface for iface in SSH_INTERFACES if iface != '0.0.0.0'])
print " ssh%s: %s" % (ifaces, ports)
if SSL_ENABLED:
ports = ", ".join([str(port) for port in SSL_PORTS])
ifaces = ",".join([" %s" % iface for iface in SSL_INTERFACES if iface != '0.0.0.0'])
print " ssl%s: %s" % (ifaces, ports)
if WEBSERVER_ENABLED:
clientstring = ""
if WEBCLIENT_ENABLED:
clientstring = '/client'
ports = ", ".join([str(port) for port in WEBSERVER_PORTS])
ifaces = ",".join([" %s" % iface for iface in WEBSERVER_INTERFACES if iface != '0.0.0.0'])
print " webserver%s%s: %s" % (clientstring, ifaces, ports)
print _(' %(servername)s Portal (%(version)s) started.') % {'servername': SERVERNAME, 'version': VERSION}
print ' amp (Portal): %s' % AMP_PORT
def shutdown(self, message="{rThe server has been shutdown. Disconnecting.{n", _abrupt=False):
def set_restart_mode(self, mode=None):
"""
If called directly, this disconnects everyone cleanly and shuts down the
reactor. If the server is killed by other means (Ctrl-C, reboot etc), this
might be called as a callback, at which point the reactor is already dead
and should not be tried to stop again (_abrupt=True).
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.
message - message to send to all connected sessions
_abrupt - only to be used by internal callback_mechanism.
Either way, the active restart setting (Restart=True/False) is
returned so the server knows which more it's in.
"""
if mode == None:
if os.path.exists(SERVER_RESTART) and 'True' == open(SERVER_RESTART, 'r').read():
mode = 'reload'
else:
mode = 'shutdown'
else:
restart = mode in ('reload', 'reset')
f = open(SERVER_RESTART, 'w')
f.write(str(restart))
f.close()
return mode
def shutdown(self, mode=None, _abrupt=False):
"""
SESSIONS.disconnect_all_sessions(reason=message)
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.
_abrupt - this is set if server is stopped by a kill command,
in which case the reactor is dead anyway.
"""
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
[(o.typeclass(o), o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()]
[(p.typeclass(p), p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()]
[(s.typeclass(s), s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()]
ServerConfig.objects.conf("server_restart_mode", "reload")
else:
if mode == 'reset':
# don't call disconnect hooks on reset
[(o.typeclass(o), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
else: # shutdown
[(o.typeclass(o), o.at_disconnect(), o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
[(p.typeclass(p), p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()]
[(s.typeclass(s), s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()]
ServerConfig.objects.conf("server_restart_mode", "reset")
if not _abrupt:
reactor.callLater(0, reactor.stop)
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
# for Windows we need to remove pid files manually
os.remove(SERVER_PIDFILE)
#------------------------------------------------------------
#
# Start the Evennia game server and add all active services
@ -208,108 +232,24 @@ application = service.Application('Evennia')
# and is where we store all the other services.
EVENNIA = Evennia(application)
# We group all the various services under the same twisted app.
# These will gradually be started as they are initialized below.
# 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.
if TELNET_ENABLED:
if AMP_ENABLED:
# Start telnet game connections
from src.server import amp
from src.server import telnet
for interface in TELNET_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(TELNET_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in TELNET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.protocol = telnet.TelnetProtocol
telnet_service = internet.TCPServer(port, factory, interface=interface)
telnet_service.setName('EvenniaTelnet%s' % pstring)
EVENNIA.services.addService(telnet_service)
if SSL_ENABLED:
# Start SSL game connection (requires PyOpenSSL).
from src.server import ssl
for interface in SSL_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(SSL_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSL_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.protocol = ssl.SSLProtocol
ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface)
ssl_service.setName('EvenniaSSL%s' % pstring)
EVENNIA.services.addService(ssl_service)
if SSH_ENABLED:
# Start SSH game connections. Will create a keypair in evennia/game if necessary.
from src.server import ssh
for interface in SSH_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(SSH_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
'protocolArgs':()})
ssh_service = internet.TCPServer(port, factory, interface=interface)
ssh_service.setName('EvenniaSSH%s' % pstring)
EVENNIA.services.addService(ssh_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()
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
from src.server.webclient import WebClient
web_root.putChild("webclientdata", WebClient())
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
for interface in WEBSERVER_INTERFACES:
ifacestr = ""
if interface != '0.0.0.0' or len(WEBSERVER_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in WEBSERVER_PORTS:
pstring = "%s:%s" % (ifacestr, port)
# create the webserver
webserver = WSGIWebServer(threads, port, web_site, interface=interface)
webserver.setName('EvenniaWebServer%s' % pstring)
EVENNIA.services.addService(webserver)
if IRC_ENABLED:
# IRC channel connections
from src.comms import irc
irc.connect_all()
if IMC2_ENABLED:
# IMC2 channel connections
from src.comms import imc2
imc2.connect_all()
factory = amp.AmpServerFactory(EVENNIA)
amp_service = internet.TCPServer(AMP_PORT, factory)
amp_service.setName("EvenniaPortal")
EVENNIA.services.addService(amp_service)
# 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()

247
src/server/serversession.py Normal file
View file

@ -0,0 +1,247 @@
"""
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 Channel
from src.utils import logger
from src.commands import cmdhandler
IDLE_COMMAND = settings.IDLE_COMMAND
from src.server.session import Session
# 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 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.
"""
if not self.logged_in:
return
player = self.get_player()
character = self.get_character()
if player:
player.at_init()
if character:
character.at_init()
# start (persistent) scripts on this object
ScriptDB.objects.validate(obj=character)
def session_login(self, player):
"""
Startup mechanisms that need to run at login. This is called
by the login command (which need to have handled authentication
already before calling this method)
player - the connected player
"""
# actually do the login by assigning session data
self.player = player
self.user = player.user
self.uid = self.user.id
self.uname = self.user.username
self.logged_in = True
self.conn_time = time.time()
# Update account's last login time.
self.user.last_login = datetime.now()
self.user.save()
# 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()
character = player.character
character.at_init()
if character:
# this player has a character. Check if it's the
# first time *this character* logs in
if character.db.FIRST_LOGIN:
character.at_first_login()
del character.db.FIRST_LOGIN
# run character login hook
character.at_pre_login()
# this is always called first
player.at_init()
self.log(_('Logged in: %(self)s') % {'self': self})
# start (persistent) scripts on this object
ScriptDB.objects.validate(obj=self.player.character)
#add session to connected list
self.sessionhandler.login(self)
# post-login hooks
player.at_post_login()
if character:
character.at_post_login()
def session_disconnect(self):
"""
Clean up the session, removing it from the game and doing some
accounting. This method is used also for non-loggedin
accounts.
"""
if self.logged_in:
player = self.get_player()
character = self.get_character()
if character:
character.at_disconnect()
uaccount = player.user
uaccount.last_login = datetime.now()
uaccount.save()
self.logged_in = False
self.sessionhandler.disconnect(self)
def get_player(self):
"""
Get the player associated with this session
"""
if self.logged_in:
return self.player
else:
return None
def get_character(self):
"""
Returns the in-game character associated with this session.
This returns the typeclass of the object.
"""
player = self.get_player()
if player:
return player.character
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 = Channel.objects.get_channel(cchan[0])
cchan.msg("[%s]: %s" % (cchan.key, message))
except Exception:
pass
logger.log_infomsg(message)
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 execute_cmd(self, command_string):
"""
Execute a command string on the server.
"""
# handle the 'idle' command
if str(command_string).strip() == IDLE_COMMAND:
self.update_session_counters(idle=True)
return
# all other inputs, including empty inputs
character = self.get_character()
if character:
character.execute_cmd(command_string)
else:
if self.logged_in:
# there is no character, but we are logged in. Use player instead.
self.get_player().execute_cmd(command_string)
else:
# we are not logged in. Use special unlogged-in call.
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
self.update_session_counters()
def data_out(self, msg, data=None):
"""
Send Evennia -> Player
"""
self.sessionhandler.data_out(self, msg, data)
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.
"""
if self.logged_in:
symbol = '#'
else:
symbol = '?'
try:
address = ":".join([str(part) for part in self.address])
except Exception:
address = self.address
return "<%s> %s@%s" % (symbol, self.uname, 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, string='', data=None):
"alias for at_data_out"
self.data_out(string, data=data)

View file

@ -1,412 +1,125 @@
"""
This defines a generic session class.
All protocols should implement this class and its hook methods.
The process of first connect:
- The custom connection-handler for the respective
protocol should be called by the transport connection itself.
- The connect-handler handles whatever internal settings are needed
- The connection-handler calls session_connect()
- session_connect() setups sessions then calls session.at_connect()
Disconnecting is a bit more complex in order to avoid circular calls
depending on if the disconnect happens automatically or manually from
a command.
The process at automatic disconnect:
- The custom disconnect-handler for the respective protocol
should be called by the transport connection itself. This handler
should be defined with a keyword argument 'step' defaulting to 1.
- since step=1, the disconnect-handler calls session_disconnect()
- session_disconnect() removes session, then calls session.at_disconnect()
- session.at_disconnect() calls the custom disconnect-handler with
step=2 as argument
- since step=2, the disconnect-handler closes the connection and
performs all needed protocol cleanup.
The process of manual disconnect:
- The command/outside function calls session.session_disconnect().
- from here the process proceeds as the automatic disconnect above.
This defines a generic session class. All connection instances (both
on Portal and Server side) should inherit from this class.
"""
import time
from datetime import datetime
#from django.contrib.auth.models import User
from django.conf import settings
#from src.objects.models import ObjectDB
from src.comms.models import Channel
from src.utils import logger, reloads
from src.commands import cmdhandler
from src.server.sessionhandler import SESSIONS
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
IDLE_COMMAND = settings.IDLE_COMMAND
class IOdata(object):
"""
A simple storage object that allows for storing
new attributes on it at creation.
"""
def __init__(self, **kwargs):
"Give keyword arguments to store as new arguments on the object."
self.__dict__.update(**kwargs)
def _login(session, player):
"""
For logging a player in. Removed this from CmdConnect because ssh
wanted to call it for autologin.
"""
# We are logging in, get/setup the player object controlled by player
# Check if this is the first time the
# *player* connects (should be set by the
if player.db.FIRST_LOGIN:
player.at_first_login()
del player.db.FIRST_LOGIN
player.at_pre_login()
character = player.character
if character:
# this player has a character. Check if it's the
# first time *this character* logs in (this should be
# set by the initial create command)
if character.db.FIRST_LOGIN:
character.at_first_login()
del character.db.FIRST_LOGIN
# run character login hook
character.at_pre_login()
# actually do the login
session.session_login(player)
# post-login hooks
player.at_post_login()
if character:
character.at_post_login()
character.execute_cmd('look')
else:
player.execute_cmd('look')
#------------------------------------------------------------
# SessionBase class
# Server Session
#------------------------------------------------------------
class SessionBase(object):
class Session(object):
"""
This class represents a player's session and is a template for
individual protocols to communicate with Evennia.
This class represents a player's session and is a template for
both portal- and server-side sessions.
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.
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.
"""
# use this to uniquely identify the protocol name, e.g. "telnet" or "comet"
protocol_key = "BaseProtocol"
# names of attributes that should be affected by syncing.
_attrs_to_sync = ['protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname',
'logged_in', 'cid', 'encoding',
'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total']
def session_connect(self, address, suid=None):
def init_session(self, protocol_key, address, sessionhandler):
"""
The setup of the session. An address (usually an IP address) on any form is required.
This should be called by the protocol at connection time.
suid = this is a session id. Needed by some transport protocols.
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
# user setup
self.name = None
# 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
self.suid = suid
# user name, for easier tracking of sessions
self.uname = None
# if user has authenticated already or not
self.logged_in = False
# database id of character/object connected to this player session (if any)
self.cid = None
self.encoding = "utf-8"
current_time = time.time()
# The time the user last issued a command.
self.cmd_last = current_time
# Player-visible idle time, excluding the IDLE command.
self.cmd_last_visible = current_time
# The time when the user connected.
self.conn_time = current_time
# Total number of commands issued.
self.cmd_total = 0
#self.channels_subscribed = {}
SESSIONS.add_unloggedin_session(self)
# calling hook
self.at_connect()
def session_login(self, player):
"""
Startup mechanisms that need to run at login
player - the connected player
"""
# 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()
character = player.character
if character:
# this player has a character. Check if it's the
# first time *this character* logs in
if character.db.FIRST_LOGIN:
character.at_first_login()
del character.db.FIRST_LOGIN
# run character login hook
character.at_pre_login()
# actually do the login by assigning session data
self.player = player
self.user = player.user
self.uid = self.user.id
self.name = self.user.username
self.logged_in = True
# 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
# 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
"""
sessdata = {}
for attrname in self._attrs_to_sync:
sessdata[attrname] = self.__dict__.get(attrname, None)
return sessdata
def load_sync_data(self, sessdata):
"""
Takes a session dictionary, as created by get_sync_data,
and loads it into the correct attributes of the session.
"""
for attrname, value in sessdata.items():
self.__dict__[attrname] = value
# Update account's last login time.
self.user.last_login = datetime.now()
self.user.save()
self.log('Logged in: %s' % self)
# start (persistent) scripts on this object
reloads.reload_scripts(obj=self.player.character)
#add session to connected list
SESSIONS.add_loggedin_session(self)
#call login hook
self.at_login(player)
# post-login hooks
player.at_post_login()
if character:
character.at_post_login()
def session_disconnect(self):
def at_sync(self):
"""
Clean up the session, removing it from the game and doing some
accounting. This method is used also for non-loggedin
accounts.
Note that this methods does not close the connection - this is protocol-dependent
and have to be done right after this function!
"""
if self.logged_in:
player = self.get_player()
uaccount = player.user
uaccount.last_login = datetime.now()
uaccount.save()
self.at_disconnect()
self.logged_in = False
SESSIONS.remove_session(self)
def session_validate(self):
"""
Validate the session to make sure they have not been idle for too long
"""
if IDLE_TIMEOUT > 0 and (time.time() - self.cmd_last) > IDLE_TIMEOUT:
self.msg("Idle timeout exceeded, disconnecting.")
self.session_disconnect()
def get_player(self):
"""
Get the player associated with this session
"""
if self.logged_in:
return self.player
else:
return None
# if self.logged_in:
# character = ObjectDB.objects.get_object_with_user(self.uid)
# if not character:
# string = "No player match for session uid: %s" % self.uid
# logger.log_errmsg(string)
# return None
# return character.player
# return None
def get_character(self):
"""
Returns the in-game character associated with a session.
This returns the typeclass of the object.
"""
player = self.get_player()
if player:
return player.character
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 = Channel.objects.get_channel(cchan[0])
cchan.msg("[%s]: %s" % (cchan.key, message))
except Exception:
pass
logger.log_infomsg(message)
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 execute_cmd(self, command_string):
"""
Execute a command string.
"""
# handle the 'idle' command
if str(command_string).strip() == IDLE_COMMAND:
self.update_session_counters(idle=True)
return
# all other inputs, including empty inputs
character = self.get_character()
if character:
# normal operation.
character.execute_cmd(command_string)
#import cProfile
#cProfile.runctx("character.execute_cmd(command_string)",
# {"command_string":command_string,"character":character}, {}, "execute_cmd.profile")
else:
if self.logged_in:
# there is no character, but we are logged in. Use player instead.
self.get_player().execute_cmd(command_string)
else:
# we are not logged in. Use special unlogged-in call.
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
self.update_session_counters()
def get_data_obj(self, **kwargs):
"""
Create a data object, storing keyword arguments on itself as arguments.
"""
return IOdata(**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.
"""
if self.logged_in:
symbol = '#'
else:
symbol = '?'
return "<%s> %s@%s" % (symbol, self.name, self.address,)
def __unicode__(self):
"""
Unicode representation
"""
return u"%s" % str(self)
#------------------------------------------------------------
# Session class - inherit from this
#------------------------------------------------------------
class Session(SessionBase):
"""
The main class to inherit from. Overload the methods here.
"""
# exchange this for a unique name you can use to identify the
# protocol type this session uses
protocol_key = "TemplateProtocol"
#
# Hook methods
#
def at_connect(self):
"""
This method is called by the connection mechanic after
connection has been made. The session is added to the
sessionhandler and basic accounting has been made at this
point.
This is the place to put e.g. welcome screens specific to the
protocol.
"""
pass
def at_login(self, player):
"""
This method is called by the login mechanic whenever the user
has finished authenticating. The user has been moved to the
right sessionhandler list and basic book keeping has been
done at this point (so logged_in=True).
Called after a session has been fully synced (including
secondary operations such as setting self.player based
on uid etc).
"""
pass
def at_disconnect(self):
"""
This method is called just before cleaning up the session
(so still logged_in=True at this point).
This method should not be called from commands, instead it
is called automatically by session_disconnect() as part of
the cleanup.
This method MUST call the protocol-dependant disconnect-handler
with step=2 to finalize the closing of the connection!
"""
# self.my-disconnect-handler(step=2)
pass
# access hooks
def at_data_in(self, string="", data=None):
def disconnect(self, reason=None):
"""
Player -> Evennia
"""
pass
def at_data_out(self, string="", data=None):
"""
Evennia -> Player
string - an string of any form to send to the player
data - a data structure of any form
generic hook called from the outside to disconnect this session
should be connected to the protocols actual disconnect mechanism.
"""
pass
# easy-access functions
def login(self, player):
"alias for at_login"
self.at_login(player)
def disconnect(self):
"alias for session_disconnect"
self.session_disconnect()
def msg(self, string='', data=None):
"alias for at_data_out"
self.at_data_out(string, data=data)
def data_out(self, msg, data=None):
"""
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, msg, data=None):
"""
hook for protocols to send incoming data to the engine.
"""
pass

View file

@ -1,31 +1,72 @@
"""
This module handles sessions of users connecting
to the server.
This module defines handlers for storing sessions when handles
sessions of users connecting to the server.
Since Evennia supports several different connection
protocols, it is important to have a joint place
to store session info. It also makes it easier
to dispatch data.
Whereas server.py handles all setup of the server
and database itself, this file handles all that
comes after initial startup.
All new sessions (of whatever protocol) are responsible for
registering themselves with this module.
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 django.contrib.auth.models import User
from src.server.models import ServerConfig
from src.utils import utils
# i18n
from django.utils.translation import ugettext as _
ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
#------------------------------------------------------------
# SessionHandler 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.
"""
sessdict = {}
for sess in self.sessions.values():
# copy all relevant data from all sessions
sessdict[sess.sessid] = sess.get_sync_data()
return sessdict
#------------------------------------------------------------
# Server-SessionHandler class
#------------------------------------------------------------
class ServerSessionHandler(SessionHandler):
"""
This object holds the stack of sessions active in the game at
any time.
@ -38,102 +79,144 @@ class SessionHandler(object):
"""
# AMP communication methods
def __init__(self):
"""
Init the handler. We track two types of sessions, those
who have just connected (unloggedin) and those who have
logged in (authenticated).
Init the handler.
"""
self.unloggedin = []
self.loggedin = []
# we keep a link to the server here, for the rest of the game to access.
self.sessions = {}
self.server = None
def add_unloggedin_session(self, session):
def portal_connect(self, sessid, session):
"""
Call at first connect. This adds a not-yet authenticated session.
"""
self.unloggedin.insert(0, session)
Called by Portal when a new session has connected.
Creates a new, unlogged-in game session.
"""
self.sessions[sessid] = session
session.execute_cmd('look')
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 session:
del self.sessions[session.sessid]
self.session_count(-1)
def portal_session_sync(self, sesslist):
"""
Syncing all session ids of the portal with the ones of the server. This is instantiated
by the portal when reconnecting.
def add_loggedin_session(self, session):
sesslist is a complete list of (sessid, session) pairs, matching the list on the portal.
if session was logged in, the amp handler will have logged them in before this point.
"""
for sess in self.sessions.values():
# we delete the old session to make sure to catch eventual lingering references.
del sess
for sess in sesslist:
self.sessions[sess.sessid] = sess
sess.at_sync()
def portal_shutdown(self):
"""
Called by server when shutting down the portal.
"""
self.server.amp_protocol.call_remote_PortalAdmin(0,
operation='SSHUTD',
data="")
# server-side access methods
def disconnect(self, session, reason=""):
"""
Called from server side to remove session and inform portal
of this fact.
"""
session = self.sessions.get(session.sessid, None)
if session:
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)
self.session_count(-1)
def login(self, session):
"""
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.
"""
# prep the session with player/user info
if not ALLOW_MULTISESSION:
# disconnect previous sessions.
self.disconnect_duplicate_sessions(session)
# store/move the session to the right list
try:
self.unloggedin.remove(session)
except ValueError:
pass
self.loggedin.insert(0, session)
session.logged_in = True
self.session_count(1)
# sync the portal to this session
sessdata = session.get_sync_data()
self.server.amp_protocol.call_remote_PortalAdmin(session.sessid,
operation='SLOGIN',
data=sessdata)
def session_sync(self):
"""
This is called by the server when it reboots. It syncs all session data
to the portal.
"""
sessdata = self.get_all_sync_data()
self.server.amp_protocol.call_remote_PortalAdmin(0,
'SSYNC',
data=sessdata)
def remove_session(self, session):
"""
Remove session from the handler
"""
removed = False
try:
self.unloggedin.remove(session)
except Exception:
try:
self.loggedin.remove(session)
except Exception:
return
self.session_count(-1)
def get_sessions(self, include_unloggedin=False):
"""
Returns the connected session objects.
"""
if include_unloggedin:
return self.loggedin + self.unloggedin
else:
return self.loggedin
def disconnect_all_sessions(self, reason="You have been disconnected."):
"""
Cleanly disconnect all of the connected sessions.
"""
sessions = self.get_sessions(include_unloggedin=True)
for session in sessions:
session.at_data_out(reason)
session.session_disconnect()
for session in self.sessions:
del session
self.session_count(0)
# 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):
"""
Disconnects any existing sessions with the same game object.
"""
reason = "Your account has been logged in from elsewhere. Disconnecting."
curr_char = curr_session.get_character()
doublet_sessions = [sess for sess in self.get_sessions()
if sess.get_character() == curr_char and sess != curr_session]
logged_out = 0
for session in doublet_sessions:
session.msg(reason)
self.remove_session(session)
logged_out += 1
self.session_count(-logged_out)
return logged_out
doublet_sessions = [sess for sess in self.sessions
if sess.logged_in
and sess.get_character() == curr_char
and sess != curr_session]
reason = _("Logged in from elsewhere. Disconnecting.")
for sessid in doublet_sessions:
self.disconnect(session, reason)
self.session_count(-1)
def validate_sessions(self):
"""
Check all currently connected sessions (logged in and not)
and see if any are dead.
"""
for session in self.get_sessions(include_unloggedin=True):
session.session_validate()
tcurr = time.time()
invalid_sessions = [session for session in self.sessions.values()
if session.logged_in and IDLE_TIMEOUT > 0
and (tcurr - session.cmd_last) > IDLE_TIMEOUT]
for session in invalid_sessions:
self.disconnect(session, reason=_("Idle timeout exceeded, disconnecting."))
self.session_count(-1)
def session_count(self, num=None):
"""
Count up/down the number of connected, authenticated users.
@ -160,7 +243,7 @@ class SessionHandler(object):
may have more than one session connected if ALLOW_MULTISESSION is True)
Only logged-in players are counted here.
"""
return len(set(sess.uid for sess in self.get_sessions()))
return len(set(session.uid for session in self.sessions.values() if session.logged_in))
def sessions_from_player(self, player):
"""
@ -172,7 +255,7 @@ class SessionHandler(object):
except User.DoesNotExist:
return None
uid = uobj.id
return [session for session in self.loggedin if session.uid == uid]
return [session for session in self.sessions.values() if session.logged_in and session.uid == uid]
def sessions_from_character(self, character):
"""
@ -183,20 +266,129 @@ class SessionHandler(object):
return self.sessions_from_player(player)
return None
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 sess.suid and sess.suid == suid]
def announce_all(self, message):
"""
Send message to all connected sessions
"""
for sess in self.get_sessions(include_unloggedin=True):
sess.msg(message)
for sess in self.sessions.values():
self.data_out(sess, message)
SESSIONS = SessionHandler()
def data_out(self, session, string="", data=""):
"""
Sending data Server -> Portal
"""
self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid,
msg=string,
data=data)
def data_in(self, sessid, string="", data=""):
"""
Data Portal -> Server
"""
session = self.sessions.get(sessid, None)
if session:
session.execute_cmd(string)
# ignore 'data' argument for now; this is otherwise the place
# to put custom effects on the server due to data input, e.g.
# from a custom client.
#------------------------------------------------------------
# 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
def connect(self, session):
"""
Called by protocol at first connect. This adds a not-yet authenticated session
using an ever-increasing counter for sessid.
"""
self.latest_sessid += 1
sessid = self.latest_sessid
session.sessid = sessid
sessdata = session.get_sync_data()
self.sessions[sessid] = session
# sync with server-side
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation="PCONN",
data=sessdata)
def disconnect(self, session):
"""
Called from portal side when the connection is closed from the portal side.
"""
sessid = session.sessid
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation="PDISCONN")
def server_disconnect(self, sessid, reason=""):
"""
Called by server to force a disconnect by sessid
"""
session = self.sessions.get(sessid, None)
if session:
session.disconnect(reason)
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
def session_from_suid(self, suid):
"""
Given a session id, retrieve the session (this is primarily
intended to be called by web clients)
"""
return [sess for sess in self.get_sessions(include_unloggedin=True)
if hasattr(sess, 'suid') and sess.suid == suid]
def data_in(self, session, string="", data=""):
"""
Called by portal sessions for relaying data coming
in from the protocol to the server. data is
serialized before passed on.
"""
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
msg=string,
data=data)
def announce_all(self, message):
"""
Send message to all connection sessions
"""
for session in self.sessions.values():
session.data_out(message)
def data_out(self, sessid, string="", data=""):
"""
Called by server for having the portal relay messages and data
to the correct session protocol.
"""
session = self.sessions.get(sessid, None)
if session:
session.data_out(string, data=data)
SESSIONS = ServerSessionHandler()
PORTAL_SESSIONS = PortalSessionHandler()

View file

@ -6,6 +6,8 @@ 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
@ -25,7 +27,9 @@ from django.conf import settings
from src.server import session
from src.players.models import PlayerDB
from src.utils import ansi, utils, logger
#from src.commands.default.unloggedin import _login
# i18n
from django.utils.translation import ugettext as _
ENCODINGS = settings.ENCODINGS
@ -40,13 +44,13 @@ class SshProtocol(Manhole, session.Session):
them. All communication between game and player goes through
here.
"""
def __init__(self, player):
def __init__(self, starttuple):
"""
For setting up the player. If player is not None then we'll
login automatically.
"""
self.player = player
self.authenticated_player = starttuple[0]
self.cfactory = starttuple[1] # obs may not be called self.factory, it gets overwritten!
def terminalSize(self, width, height):
"""
@ -60,10 +64,14 @@ class SshProtocol(Manhole, session.Session):
self.height = height
# initialize the session
self.session_connect(self.getClientAddress())
if self.player is not None:
self.session_login(self.player)
self.execute_cmd('look')
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):
"""
@ -74,8 +82,9 @@ class SshProtocol(Manhole, session.Session):
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
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
# initalize
def handle_INT(self):
"""
@ -116,32 +125,22 @@ class SshProtocol(Manhole, session.Session):
self.terminal.loseConnection()
def connectionLost(self, reason=None, step=1):
def connectionLost(self, reason=None):
"""
This is executed when the connection is lost for
whatever reason.
whatever reason. It can also be called directly,
from the disconnect method.
Closing the connection takes two steps
step 1 - is the default and is used when this method is
called automatically. The method should then call self.session_disconnect().
Step 2 - means this method is called from at_disconnect(). At this point
the sessions are assumed to have been handled, and so the transport can close
without further ado.
"""
insults.TerminalProtocol.connectionLost(self, reason)
if step == 1:
self.session_disconnect()
else:
self.terminal.loseConnection()
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()
@ -152,7 +151,7 @@ class SshProtocol(Manhole, session.Session):
command for the purpose of the MUD. So we take the user input
and pass it on to the game engine.
"""
self.at_data_in(string)
self.sessionhandler.data_in(self, string)
def lineSend(self, string):
"""
@ -166,35 +165,18 @@ class SshProtocol(Manhole, session.Session):
self.terminal.write(line) #this is the telnet-specific method for sending
self.terminal.nextLine()
# session-general method hooks
def at_connect(self):
"""
Show the banner screen.
"""
self.telnet_markup = True
# show connection screen
def at_login(self, player):
"""
Called after authentication. self.logged_in=True at this point.
"""
if player.has_attribute('telnet_markup'):
self.telnet_markup = player.get_attribute("telnet_markup")
else:
self.telnet_markup = True
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
def disconnect(self, reason="Connection closed. Goodbye for now."):
"""
Disconnect from server
"""
char = self.get_character()
if char:
char.at_disconnect()
self.at_data_out(reason)
self.connectionLost(step=2)
if reason:
self.data_out(reason)
self.connectionLost(reason)
def at_data_out(self, string, data=None):
def data_out(self, string, data=None):
"""
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
"""
@ -203,31 +185,18 @@ class SshProtocol(Manhole, session.Session):
except Exception, e:
self.lineSend(str(e))
return
nomarkup = not self.telnet_markup
raw = False
nomarkup = False
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", self.telnet_markup)
raw = data.get("raw", False)
# check if we want to remove all markup
nomarkup = data.get("nomarkup", not self.telnet_markup)
nomarkup = data.get("nomarkup", False)
if raw:
self.lineSend(string)
else:
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
def at_data_in(self, string, data=None):
"""
Line from Player -> Evennia. 'data' argument is not used.
"""
try:
string = utils.to_unicode(string, encoding=self.encoding)
self.execute_cmd(string)
return
except Exception, e:
logger.log_errmsg(str(e))
class ExtraInfoAuthServer(SSHUserAuthServer):
def auth_password(self, packet):
@ -251,15 +220,19 @@ class PlayerDBPasswordChecker(object):
"""
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
res = (None, self.factory)
if player and player.user.check_password(password):
res = player
res = (player, self.factory)
return defer.succeed(res)
class PassAvatarIdTerminalRealm(TerminalRealm):
@ -322,7 +295,7 @@ def getKeyPair(pubkeyfile, privkeyfile):
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
# No keypair exists. Generate a new RSA keypair
print " Generating SSH RSA keypair ...",
print _(" Generating SSH RSA keypair ..."),
from Crypto.PublicKey import RSA
KEY_LENGTH = 1024
@ -359,6 +332,7 @@ def makeFactory(configdict):
rlm.transportFactory = TerminalSessionTransport_getPeer
rlm.chainedProtocolFactory = chainProtocolFactory
factory = ConchFactory(Portal(rlm))
factory.sessionhandler = configdict['sessions']
try:
# create/get RSA keypair
@ -366,12 +340,12 @@ def makeFactory(configdict):
factory.publicKeys = {'ssh-rsa': publicKey}
factory.privateKeys = {'ssh-rsa': privateKey}
except Exception, e:
print " getKeyPair error: %s\n WARNING: Evennia could not auto-generate SSH keypair. Using conch default keys instead." % e
print " If this error persists, create game/%s and game/%s yourself using third-party tools." % (pubkeyfile, privkeyfile)
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.portal.registerChecker(PlayerDBPasswordChecker(factory))
return factory

View file

@ -8,8 +8,8 @@ from twisted.internet import ssl as twisted_ssl
try:
import OpenSSL
except ImportError:
print " SSL_ENABLED requires PyOpenSSL."
sys.exit()
print _(" SSL_ENABLED requires PyOpenSSL.")
sys.exit(5)
from src.server.telnet import TelnetProtocol
@ -33,7 +33,7 @@ def verify_SSL_key_and_cert(keyfile, certfile):
from Crypto.PublicKey import RSA
from twisted.conch.ssh.keys import Key
print " Creating SSL key and certificate ... ",
print _(" Creating SSL key and certificate ... "),
try:
# create the RSA key and store it.
@ -42,9 +42,9 @@ def verify_SSL_key_and_cert(keyfile, certfile):
keyString = rsaKey.toString(type="OPENSSH")
file(keyfile, 'w+b').write(keyString)
except Exception,e:
print "rsaKey error: %s\n WARNING: Evennia could not auto-generate SSL private key." % e
print "If this error persists, create game/%s yourself using third-party tools." % keyfile
sys.exit()
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
@ -56,12 +56,12 @@ def verify_SSL_key_and_cert(keyfile, certfile):
err = subprocess.call(exestring)#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except OSError, e:
print " %s\n" % e
print " Evennia's SSL context factory could not automatically create an SSL certificate game/%s." % certfile
print " A private key 'ssl.key' was already created. Please create %s manually using the commands valid " % certfile
print " for your operating system."
print " Example (linux, using the openssl program): "
print " %s" % exestring
sys.exit()
print _(" Evennia's SSL context factory could not automatically create an SSL certificate game/%(cert)s.") % {'cert': certfile}
print _(" A private key 'ssl.key' was already created. Please create %(cert)s manually using the commands valid") % {'cert': certfile}
print _(" for your operating system.")
print _(" Example (linux, using the openssl program): ")
print " %s" % exestring
sys.exit(5)
print "done."
def getSSLContext():

View file

@ -8,130 +8,73 @@ sessions etc.
"""
from twisted.conch.telnet import StatefulTelnetProtocol
from django.conf import settings
from src.server import session
from src.utils import ansi, utils, logger
from src.server.session import Session
from src.utils import utils, ansi
ENCODINGS = settings.ENCODINGS
class TelnetProtocol(StatefulTelnetProtocol, session.Session):
class TelnetProtocol(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.
"""
# telnet-specific hooks
def connectionMade(self):
"""
This is called when the connection is first
established.
"""
# initialize the session
self.session_connect(self.getClientAddress())
client_address = self.transport.client
self.init_session("telnet", client_address, self.factory.sessionhandler)
# add us to sessionhandler
self.sessionhandler.connect(self)
def connectionLost(self, reason=None, step=1):
def connectionLost(self, reason):
"""
This is executed when the connection is lost for
whatever reason.
whatever reason. It can also be called directly, from
the disconnect method
"""
self.sessionhandler.disconnect(self)
self.transport.loseConnection()
Closing the connection takes two steps
step 1 - is the default and is used when this method is
called automatically. The method should then call self.session_disconnect().
Step 2 - means this method is called from at_disconnect(). At this point
the sessions are assumed to have been handled, and so the transport can close
without further ado.
"""
if step == 1:
self.session_disconnect()
else:
self.transport.loseConnection()
def getClientAddress(self):
"""
Returns the client's address and port in a tuple. For example
('127.0.0.1', 41917)
"""
return self.transport.client
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.
Telnet method called when data is coming in over the telnet
connection. We pass it on to the game engine directly.
"""
self.at_data_in(string)
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.
# Session hooks
def disconnect(self, reason=None):
"""
self.sendLine(string) #this is the telnet-specific method for sending
generic hook for the engine to call in order to
disconnect this protocol.
"""
if reason:
self.data_out(reason)
self.connectionLost(reason)
# session-general method hooks
def at_connect(self):
def data_out(self, string, data=None):
"""
Show the banner screen.
generic hook method for engine to call in order to send data
through the telnet connection.
Data Evennia -> Player. 'data' argument is not used
"""
self.telnet_markup = True
# show connection screen
self.execute_cmd('look')
def at_login(self, player):
"""
Called after authentication. self.logged_in=True at this point.
"""
if player.has_attribute('telnet_markup'):
self.telnet_markup = player.get_attribute("telnet_markup")
else:
self.telnet_markup = True
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
"""
Disconnect from server
"""
char = self.get_character()
if char:
char.at_disconnect()
self.at_data_out(reason)
self.connectionLost(step=2)
def at_data_out(self, string, data=None):
"""
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
"""
try:
string = utils.to_str(string, encoding=self.encoding)
except Exception, e:
self.lineSend(str(e))
return
nomarkup = not self.telnet_markup
raw = False
if type(data) == dict:
try:
string = utils.to_str(string, encoding=self.encoding)
except Exception, e:
self.sendLine(str(e))
return
nomarkup = False
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", self.telnet_markup)
raw = data.get("raw", False)
# check if we want to remove all markup
nomarkup = data.get("nomarkup", not self.telnet_markup)
if raw:
self.lineSend(string)
else:
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
def at_data_in(self, string, data=None):
"""
Line from Player -> Evennia. 'data' argument is not used.
"""
try:
string = utils.to_unicode(string, encoding=self.encoding)
self.execute_cmd(string)
return
except Exception, e:
logger.log_errmsg(str(e))
nomarkup = data.get("nomarkup", False)
if raw:
self.sendLine(string)
else:
self.sendLine(ansi.parse_ansi(string, strip_ansi=nomarkup))

View file

@ -29,7 +29,6 @@ from django.conf import settings
from src.utils import utils, logger, ansi
from src.utils.text2html import parse_html
from src.server import session
from src.server.sessionhandler import SESSIONS
SERVERNAME = settings.SERVERNAME
ENCODINGS = settings.ENCODINGS
@ -55,7 +54,7 @@ def jsonify(obj):
class WebClient(resource.Resource):
"""
An ajax/comet long-polling transport protocol for
An ajax/comet long-polling transport
"""
isLeaf = True
allowedMethods = ('POST',)
@ -95,24 +94,16 @@ class WebClient(resource.Resource):
dataentries.append(jsonify({'msg':string, 'data':data}))
self.databuffer[suid] = dataentries
def disconnect(self, suid, step=1):
def client_disconnect(self, suid):
"""
Disconnect session with given suid.
step 1 : call session_disconnect()
step 2 : finalize disconnection
"""
if step == 1:
sess = SESSIONS.session_from_suid(suid)
sess[0].session_disconnect()
else:
if self.requests.has_key(suid):
for request in self.requests.get(suid, []):
request.finish()
del self.requests[suid]
if self.databuffer.has_key(suid):
del self.databuffer[suid]
if self.requests.has_key(suid):
for request in self.requests.get(suid, []):
request.finish()
del self.requests[suid]
if self.databuffer.has_key(suid):
del self.databuffer[suid]
def mode_init(self, request):
"""
@ -133,7 +124,9 @@ class WebClient(resource.Resource):
sess = WebClientSession()
sess.client = self
sess.session_connect(remote_addr, suid)
sess.init_session("comet", remote_addr, self.sessionhandler)
sess.suid = suid
sess.sessionhandler.connect(sess)
return jsonify({'msg':host_string, 'suid':suid})
def mode_input(self, request):
@ -144,11 +137,12 @@ class WebClient(resource.Resource):
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
return ''
sess = SESSIONS.session_from_suid(suid)
sess = self.sessionhandler.session_from_suid(suid)
if sess:
sess = sess[0]
string = request.args.get('msg', [''])[0]
data = request.args.get('data', [None])[0]
sess[0].at_data_in(string, data)
sess.sessionhandler.data_in(sess, string, data)
return ''
def mode_receive(self, request):
@ -179,7 +173,7 @@ class WebClient(resource.Resource):
"""
suid = request.args.get('suid', ['0'])[0]
if suid == '0':
self.disconnect(suid)
self.client_disconnect(suid)
return ''
def render_POST(self, request):
@ -217,35 +211,16 @@ class WebClientSession(session.Session):
"""
This represents a session running in a webclient.
"""
def at_connect(self):
"""
Show the banner screen.
"""
# show screen
self.telnet_markup = True
self.execute_cmd('look')
def at_login(self, player):
"""
Called after authentication. self.logged_in=True at this point.
"""
if player.has_attribute('telnet_markup'):
self.telnet_markup = player.get_attribute("telnet_markup")
def at_disconnect(self, reason=None):
def disconnect(self, reason=None):
"""
Disconnect from server
"""
if reason:
self.lineSend(self.suid, reason)
char = self.get_character()
if char:
char.at_disconnect()
self.client.disconnect(self.suid, step=2)
self.client.client_disconnect(self.suid)
def at_data_out(self, string='', data=None):
def data_out(self, string='', data=None):
"""
Data Evennia -> Player access hook.
@ -261,13 +236,13 @@ class WebClientSession(session.Session):
try:
string = utils.to_str(string, encoding=self.encoding)
nomarkup = not self.telnet_markup
nomarkup = False
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", self.telnet_markup)
raw = data.get("raw", False)
# check if we want to remove all markup
nomarkup = data.get("nomarkup", not self.telnet_markup)
nomarkup = data.get("nomarkup", False)
if raw:
self.client.lineSend(self.suid, string)
else:
@ -275,21 +250,3 @@ class WebClientSession(session.Session):
return
except Exception, e:
logger.log_trace()
def at_data_in(self, string, data=None):
"""
Input from Player -> Evennia (called by client protocol).
Use of 'data' is up to the client - server implementation.
"""
# treat data?
if data:
pass
# the string part is identical to telnet
try:
string = utils.to_unicode(string, encoding=self.encoding)
self.execute_cmd(string)
return
except Exception, e:
logger.log_trace()