From d621239ff6246cdc5038269622b08cfed1dfe8f9 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sun, 12 Apr 2020 10:24:31 -0700 Subject: [PATCH] Made almost the entire networking guts replaceable. --- CHANGELOG.md | 1 + evennia/server/portal/amp.py | 2 +- evennia/server/portal/portal.py | 14 ++-- evennia/server/portal/portalsessionhandler.py | 8 ++- evennia/server/portal/ssh.py | 7 +- evennia/server/portal/ssl.py | 6 +- evennia/server/portal/telnet.py | 9 ++- evennia/server/portal/webclient.py | 8 +-- evennia/server/session.py | 19 +----- evennia/server/sessionhandler.py | 7 +- evennia/settings_default.py | 67 ++++++++++++++++++- 11 files changed, 106 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ffa74b003..57876a7524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - New `utils.format_grid` for easily displaying long lists of items in a block. - Using `lunr` search indexing for better `help` matching and suggestions. Also improve the main help command's default listing output. +- Made most of the networking classes such as Protocols and the SessionHandlers replaceable via `settings.py` for modding enthusiasts. ### Already in master diff --git a/evennia/server/portal/amp.py b/evennia/server/portal/amp.py index 505ea5e3e0..c49ac4e55e 100644 --- a/evennia/server/portal/amp.py +++ b/evennia/server/portal/amp.py @@ -15,7 +15,7 @@ import zlib # Used in Compressed class import pickle from twisted.internet.defer import DeferredList, Deferred -from evennia.utils.utils import to_str, variable_from_module +from evennia.utils.utils import variable_from_module # delayed import _LOGGER = None diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 633e63bb51..e16a9b6910 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -25,7 +25,7 @@ import evennia evennia._init() -from evennia.utils.utils import get_evennia_version, mod_import, make_iter +from evennia.utils.utils import get_evennia_version, mod_import, make_iter, class_from_module from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS from evennia.utils import logger from evennia.server.webserver import EvenniaReverseProxyResource @@ -261,6 +261,7 @@ if TELNET_ENABLED: # Start telnet game connections from evennia.server.portal import telnet + _telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS) for interface in TELNET_INTERFACES: ifacestr = "" @@ -270,7 +271,7 @@ if TELNET_ENABLED: pstring = "%s:%s" % (ifacestr, port) factory = telnet.TelnetServerFactory() factory.noisy = False - factory.protocol = telnet.TelnetProtocol + factory.protocol = _telnet_protocol factory.sessionhandler = PORTAL_SESSIONS telnet_service = internet.TCPServer(port, factory, interface=interface) telnet_service.setName("EvenniaTelnet%s" % pstring) @@ -284,6 +285,7 @@ if SSL_ENABLED: # Start Telnet+SSL game connection (requires PyOpenSSL). from evennia.server.portal import telnet_ssl + _ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS) for interface in SSL_INTERFACES: ifacestr = "" @@ -294,7 +296,7 @@ if SSL_ENABLED: factory = protocol.ServerFactory() factory.noisy = False factory.sessionhandler = PORTAL_SESSIONS - factory.protocol = telnet_ssl.SSLProtocol + factory.protocol = _ssl_protocol ssl_context = telnet_ssl.getSSLContext() if ssl_context: @@ -317,6 +319,7 @@ if SSH_ENABLED: # evennia/game if necessary. from evennia.server.portal import ssh + _ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS) for interface in SSH_INTERFACES: ifacestr = "" @@ -326,7 +329,7 @@ if SSH_ENABLED: pstring = "%s:%s" % (ifacestr, port) factory = ssh.makeFactory( { - "protocolFactory": ssh.SshProtocol, + "protocolFactory": _ssh_protocol, "protocolArgs": (), "sessions": PORTAL_SESSIONS, } @@ -345,6 +348,7 @@ if WEBSERVER_ENABLED: # Start a reverse proxy to relay data to the Server-side webserver websocket_started = False + _websocket_protocol = class_from_module(settings.WEBSOCKET_PROTOCOL_CLASS) for interface in WEBSERVER_INTERFACES: ifacestr = "" if interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1: @@ -379,7 +383,7 @@ if WEBSERVER_ENABLED: factory = Websocket() factory.noisy = False - factory.protocol = webclient.WebSocketClient + factory.protocol = _websocket_protocol factory.sessionhandler = PORTAL_SESSIONS websocket_service = internet.TCPServer(port, factory, interface=w_interface) websocket_service.setName("EvenniaWebSocket%s:%s" % (w_ifacestr, port)) diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 0f4c6cb560..dd6fadfc24 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -9,6 +9,7 @@ from twisted.internet import reactor from django.conf import settings from evennia.server.sessionhandler import SessionHandler, PCONN, PDISCONN, PCONNSYNC, PDISCONNALL from evennia.utils.logger import log_trace +from evennia.utils.utils import class_from_module # module import _MOD_IMPORT = None @@ -32,9 +33,10 @@ DUMMYSESSION = namedtuple("DummySession", ["sessid"])(0) # ------------------------------------------------------------- # Portal-SessionHandler class # ------------------------------------------------------------- +_BASE_HANDLER_CLASS = class_from_module(settings.SERVER_SESSION_HANDLER_CLASS) -class PortalSessionHandler(SessionHandler): +class PortalSessionHandler(_BASE_HANDLER_CLASS): """ This object holds the sessions connected to the portal at any time. It is synced with the server's equivalent SessionHandler over the AMP @@ -463,4 +465,6 @@ class PortalSessionHandler(SessionHandler): log_trace() -PORTAL_SESSIONS = PortalSessionHandler() +_portal_sessionhandler_class = class_from_module(settings.PORTAL_SESSION_HANDLER_CLASS) + +PORTAL_SESSIONS = _portal_sessionhandler_class() diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index b2a891710f..3a47dde433 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -43,10 +43,9 @@ from twisted.conch import interfaces as iconch from twisted.python import components from django.conf import settings -from evennia.server import session from evennia.accounts.models import AccountDB from evennia.utils import ansi -from evennia.utils.utils import to_str +from evennia.utils.utils import to_str, class_from_module _RE_N = re.compile(r"\|n$") _RE_SCREENREADER_REGEX = re.compile( @@ -74,6 +73,8 @@ and put them here: _PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE ) +_BASE_SESSION = class_from_module(settings.BASE_SESSION_CLASS) + # not used atm class SSHServerFactory(protocol.ServerFactory): @@ -84,7 +85,7 @@ class SSHServerFactory(protocol.ServerFactory): return "SSH" -class SshProtocol(Manhole, session.Session): +class SshProtocol(Manhole, _BASE_SESSION): """ Each account connecting over ssh gets this protocol assigned to them. All communication between game and account goes through diff --git a/evennia/server/portal/ssl.py b/evennia/server/portal/ssl.py index 44642bd9e9..03aea685d4 100644 --- a/evennia/server/portal/ssl.py +++ b/evennia/server/portal/ssl.py @@ -18,7 +18,7 @@ except ImportError as error: raise ImportError(errstr.format(err=error)) from django.conf import settings -from evennia.server.portal.telnet import TelnetProtocol +from evennia.utils.utils import class_from_module _GAME_DIR = settings.GAME_DIR @@ -43,8 +43,10 @@ example (linux, using the openssl program): {exestring} """ +_TELNET_PROTOCOL = class_from_module(settings.TELNET_PROTOCOL_CLASS) -class SSLProtocol(TelnetProtocol): + +class SSLProtocol(_TELNET_PROTOCOL): """ Communication is the same as telnet, except data transfer is done with encryption. diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 4dbee60f7d..c45f57d6e0 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -25,12 +25,11 @@ from twisted.conch.telnet import ( LINEMODE_TRAPSIG, ) from django.conf import settings -from evennia.server.session import Session from evennia.server.portal import ttype, mssp, telnet_oob, naws, suppress_ga from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP from evennia.server.portal.mxp import Mxp, mxp_parse from evennia.utils import ansi -from evennia.utils.utils import to_bytes +from evennia.utils.utils import to_bytes, class_from_module _RE_N = re.compile(r"\|n$") _RE_LEND = re.compile(br"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE) @@ -56,6 +55,10 @@ _HTTP_WARNING = bytes( ) +_BASE_SESSION = class_from_module(settings.BASE_SESSION_CLASS) + + + class TelnetServerFactory(protocol.ServerFactory): "This is only to name this better in logs" noisy = False @@ -64,7 +67,7 @@ class TelnetServerFactory(protocol.ServerFactory): return "Telnet" -class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): +class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION): """ Each player connecting over telnet (ie using most traditional mud clients) gets a telnet protocol instance assigned to them. All diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index faee4c20f4..d56e0a2815 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -17,10 +17,8 @@ from the command line and interprets it as an Evennia Command: `["text", ["look" import re import json import html -from twisted.internet.protocol import Protocol from django.conf import settings -from evennia.server.session import Session -from evennia.utils.utils import to_str, mod_import +from evennia.utils.utils import mod_import, class_from_module from evennia.utils.ansi import parse_ansi from evennia.utils.text2html import parse_html from autobahn.twisted.websocket import WebSocketServerProtocol @@ -39,8 +37,10 @@ CLOSE_NORMAL = WebSocketServerProtocol.CLOSE_STATUS_CODE_NORMAL # called when the browser is navigating away from the page GOING_AWAY = WebSocketServerProtocol.CLOSE_STATUS_CODE_GOING_AWAY +_BASE_SESSION = class_from_module(settings.BASE_SESSION_CLASS) -class WebSocketClient(WebSocketServerProtocol, Session): + +class WebSocketClient(WebSocketServerProtocol, _BASE_SESSION): """ Implements the server-side of the Websocket connection. """ diff --git a/evennia/server/session.py b/evennia/server/session.py index dab0f4b705..093472e820 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -36,24 +36,7 @@ class Session(object): """ # names of attributes that should be affected by syncing. - _attrs_to_sync = ( - "protocol_key", - "address", - "suid", - "sessid", - "uid", - "csessid", - "uname", - "logged_in", - "puid", - "conn_time", - "cmd_last", - "cmd_last_visible", - "cmd_total", - "protocol_flags", - "server_data", - "cmdset_storage_string", - ) + _attrs_to_sync = settings.SESSION_SYNC_ATTRS def init_session(self, protocol_key, address, sessionhandler): """ diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 0d099d6d30..23850318f2 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -23,6 +23,7 @@ from evennia.utils.utils import ( make_iter, delay, callables_from_module, + class_from_module ) from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT @@ -857,5 +858,9 @@ class ServerSessionHandler(SessionHandler): log_trace() -SESSION_HANDLER = ServerSessionHandler() +# import class from settings +_session_handler_class = class_from_module(settings.SERVER_SESSION_HANDLER_CLASS) + +# Instantiate class. These globals are used to provide singleton-like behavior. +SESSION_HANDLER = _session_handler_class() SESSIONS = SESSION_HANDLER # legacy diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 751791b549..b7778161eb 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -465,9 +465,6 @@ CHANNEL_COMMAND_CLASS = "evennia.comms.channelhandler.ChannelCommand" # Typeclasses and other paths ###################################################################### -# Server-side session class used. -SERVER_SESSION_CLASS = "evennia.server.serversession.ServerSession" - # These are paths that will be prefixed to the paths given if the # immediately entered path fail to find a typeclass. It allows for # shorter input strings. They must either base off the game directory @@ -511,6 +508,70 @@ START_LOCATION = "#2" # issues. TYPECLASS_AGGRESSIVE_CACHE = True +###################################################################### +# Networking Replaceable Guts +###################################################################### +# Modify the things below at your own risk. This is here be dragons territory. + +# The Base Session Class is used as a parent class for all Protocols such as +# Telnet and SSH.) Changing this could be really dangerous. It will cascade +# to tons of classes. You generally shouldn't need to touch protocols. +BASE_SESSION_CLASS = "evennia.server.session.Session" + +# Telnet Protocol inherits from whatever above BASE_SESSION_CLASS is specified. +# It is used for all telnet connections, and is also inherited by the SSL Protocol +# (which is just TLS + Telnet). +TELNET_PROTOCOL_CLASS = "evennia.server.portal.telnet.TelnetProtocol" +SSL_PROTOCOL_CLASS = "evennia.server.portal.ssl.SSLProtocol" + +# Websocket Client Protocol. This inherits from BASE_SESSION_CLASS. It is used +# for all webclient connections. +WEBSOCKET_PROTOCOL_CLASS = "evennia.server.portal.webclient.WebSocketClient" + +# Protocol for the SSH interface. This inherits from BASE_SESSION_CLASS. +SSH_PROTOCOL_CLASS = "evennia.server.portal.ssh.SshProtocol" + +# Server-side session class used. This will inherit from BASE_SESSION_CLASS. +# This one isn't as dangerous to replace. +SERVER_SESSION_CLASS = "evennia.server.serversession.ServerSession" + +# The Server SessionHandler manages all ServerSessions, handling logins, +# ensuring the login process happens smoothly, handling expected and +# unexpected disconnects. You shouldn't need to touch it, but you can. +# Replace it to implement altered game logic. +SERVER_SESSION_HANDLER_CLASS = "evennia.server.sessionhandler.ServerSessionHandler" + +# The Portal SessionHandler manages all incoming connections regardless of +# the protocol in use. It is responsible for keeping them going and informing +# the Server Session Handler of the connections and synchronizing them across the +# AMP connection. You shouldn't ever need to change this. But you can. +PORTAL_SESSION_HANDLER_CLASS = "evennia.server.portal.portalsessionhandler.PortalSessionHandler" + + +# These are members / properties / attributes kept on both Server and +# Portal Sessions. They are sync'd at various points, such as logins and +# reloads. If you add to this, you may need to adjust the class __init__ +# so the additions have somewhere to go. These must be simple things that +# can be pickled - stuff you could serialize to JSON is best. +SESSION_SYNC_ATTRS = ( + "protocol_key", + "address", + "suid", + "sessid", + "uid", + "csessid", + "uname", + "logged_in", + "puid", + "conn_time", + "cmd_last", + "cmd_last_visible", + "cmd_total", + "protocol_flags", + "server_data", + "cmdset_storage_string" + ) + ###################################################################### # Options and validators ######################################################################