diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index bbeec30fc5..1163133979 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -237,7 +237,7 @@ if SSL_ENABLED: # Start Telnet+SSL game connection (requires PyOpenSSL). - from evennia.server.portal import ssl + from evennia.server.portal import telnet_ssl for interface in SSL_INTERFACES: ifacestr = "" @@ -245,18 +245,24 @@ if SSL_ENABLED: ifacestr = "-%s" % interface for port in SSL_PORTS: pstring = "%s:%s" % (ifacestr, port) - factory = ssl.SSLServerFactory() + factory = telnet_ssl.SSLServerFactory() factory.noisy = False 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) + factory.protocol = telnet_ssl.SSLProtocol + ssl_context = telnet_ssl.getSSLContext() + if ssl_context: + ssl_service = internet.SSLServer(port, + factory, + telnet_ssl.getSSLContext(), + interface=interface) + ssl_service.setName('EvenniaSSL%s' % pstring) + PORTAL.services.addService(ssl_service) - INFO_DICT["telnet_ssl"].append("telnet+ssl%s: %s" % (ifacestr, port)) + INFO_DICT["telnet_ssl"].append("telnet+ssl%s: %s" % (ifacestr, port)) + print(" ssl%s: %s" % (ifacestr, port)) + else: + INFO_DICT["telnet_ssl"].append( + "telnet+ssl%s: %s (deactivated - keys/cert unset)" % (ifacestr, port)) if SSH_ENABLED: diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index 3920c795d9..715d99b292 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -52,12 +52,25 @@ from evennia.utils.utils import to_str _RE_N = re.compile(r"\|n$") _RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE) _GAME_DIR = settings.GAME_DIR +_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-private.key") +_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-public.key") +_KEY_LENGTH = 2048 CTRL_C = '\x03' CTRL_D = '\x04' CTRL_BACKSLASH = '\x1c' CTRL_L = '\x0c' +_NO_AUTOGEN = """ +Evennia could not generate SSH private- and public keys ({{err}}) +Using conch default keys instead. + +If this error persists, create the keys manually (using the tools for your OS) +and put them here: + {} + {} +""".format(_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE) + # not used atm class SSHServerFactory(protocol.ServerFactory): @@ -75,6 +88,7 @@ class SshProtocol(Manhole, session.Session): here. """ + noisy = False def __init__(self, starttuple): """ @@ -85,6 +99,7 @@ class SshProtocol(Manhole, session.Session): starttuple (tuple): A (account, factory) tuple. """ + self.protocol_key = "ssh" self.authenticated_account = starttuple[0] # obs must not be called self.factory, that gets overwritten! self.cfactory = starttuple[1] @@ -113,7 +128,7 @@ class SshProtocol(Manhole, session.Session): # since we might have authenticated already, we might set this here. if self.authenticated_account: self.logged_in = True - self.uid = self.authenticated_account.user.id + self.uid = self.authenticated_account.id self.sessionhandler.connect(self) def connectionMade(self): @@ -237,7 +252,7 @@ class SshProtocol(Manhole, session.Session): """ if reason: - self.data_out(text=reason) + self.data_out(text=((reason, ), {})) self.connectionLost(reason) def data_out(self, **kwargs): @@ -311,6 +326,9 @@ class SshProtocol(Manhole, session.Session): class ExtraInfoAuthServer(SSHUserAuthServer): + + noisy = False + def auth_password(self, packet): """ Password authentication. @@ -336,6 +354,7 @@ class AccountDBPasswordChecker(object): useful for the Realm. """ + noisy = False credentialInterfaces = (credentials.IUsernamePassword,) def __init__(self, factory): @@ -371,6 +390,8 @@ class PassAvatarIdTerminalRealm(TerminalRealm): """ + noisy = False + def _getAvatar(self, avatarId): comp = components.Componentized() user = self.userFactory(comp, avatarId) @@ -392,6 +413,8 @@ class TerminalSessionTransport_getPeer(object): """ + noisy = False + def __init__(self, proto, chainedProtocol, avatar, width, height): self.proto = proto self.avatar = avatar @@ -426,33 +449,32 @@ 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 ...", end=' ') from Crypto.PublicKey import RSA - KEY_LENGTH = 1024 - rsaKey = Key(RSA.generate(KEY_LENGTH)) - publicKeyString = rsaKey.public().toString(type="OPENSSH") - privateKeyString = rsaKey.toString(type="OPENSSH") + rsa_key = Key(RSA.generate(_KEY_LENGTH)) + public_key_string = rsa_key.public().toString(type="OPENSSH") + private_key_string = rsa_key.toString(type="OPENSSH") # save keys for the future. - file(pubkeyfile, 'w+b').write(publicKeyString) - file(privkeyfile, 'w+b').write(privateKeyString) - print(" done.") + with open(privkeyfile, 'wt') as pfile: + pfile.write(private_key_string) + print("Created SSH private key in '{}'".format(_PRIVATE_KEY_FILE)) + with open(pubkeyfile, 'wt') as pfile: + pfile.write(public_key_string) + print("Created SSH public key in '{}'".format(_PUBLIC_KEY_FILE)) else: - publicKeyString = file(pubkeyfile).read() - privateKeyString = file(privkeyfile).read() + with open(pubkeyfile) as pfile: + public_key_string = pfile.read() + with open(privkeyfile) as pfile: + private_key_string = pfile.read() - return Key.fromString(publicKeyString), Key.fromString(privateKeyString) + return Key.fromString(public_key_string), Key.fromString(private_key_string) def makeFactory(configdict): """ Creates the ssh server factory. """ - - pubkeyfile = os.path.join(_GAME_DIR, "server", "ssh-public.key") - privkeyfile = os.path.join(_GAME_DIR, "server", "ssh-private.key") - def chainProtocolFactory(username=None): return insults.ServerProtocol( configdict['protocolFactory'], @@ -467,14 +489,11 @@ def makeFactory(configdict): try: # create/get RSA keypair - publicKey, privateKey = getKeyPair(pubkeyfile, privkeyfile) + publicKey, privateKey = getKeyPair(_PUBLIC_KEY_FILE, _PRIVATE_KEY_FILE) factory.publicKeys = {'ssh-rsa': publicKey} factory.privateKeys = {'ssh-rsa': privateKey} except Exception as err: - print("getKeyPair error: {err}\n WARNING: Evennia could not " - "auto-generate SSH keypair. Using conch default keys instead.\n" - "If this error persists, create {pub} and " - "{priv} yourself using third-party tools.".format(err=err, pub=pubkeyfile, priv=privkeyfile)) + print(_NO_AUTOGEN.format(err=err)) factory.services = factory.services.copy() factory.services['ssh-userauth'] = ExtraInfoAuthServer diff --git a/evennia/server/portal/ssl.py b/evennia/server/portal/ssl.py deleted file mode 100644 index c2c6284c93..0000000000 --- a/evennia/server/portal/ssl.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -This is a simple context factory for auto-creating -SSL keys and certificates. - -""" -from __future__ import print_function - -import os -import sys -from twisted.internet import protocol -try: - import OpenSSL - from twisted.internet import ssl as twisted_ssl -except ImportError as error: - errstr = """ - {err} - SSL requires the PyOpenSSL library and dependencies: - - pip install pyopenssl pycrypto enum pyasn1 service_identity - - Stop and start Evennia again. If no certificate can be generated, you'll - get a suggestion for a (linux) command to generate this locally. - - """ - raise ImportError(errstr.format(err=error)) - -from django.conf import settings -from evennia.server.portal.telnet import TelnetProtocol - -_GAME_DIR = settings.GAME_DIR - -# messages - -NO_AUTOGEN = """ - -{err} -Evennia could not auto-generate the SSL private key. If this error -persists, create {keyfile} yourself using third-party tools. -""" - -NO_AUTOCERT = """ - -{err} -Evennia's SSL context factory could not automatically, create an SSL -certificate {certfile}. - -A private key {keyfile} was already created. Please create {certfile} -manually using the commands valid for your operating system, for -example (linux, using the openssl program): - {exestring} -""" - - -class SSLServerFactory(protocol.ServerFactory): - "This is only to name this better in logs" - noisy = False - - def logPrefix(self): - return "SSL" - - -class SSLProtocol(TelnetProtocol): - """ - Communication is the same as telnet, except data transfer - is done with encryption. - """ - - def __init__(self, *args, **kwargs): - super(SSLProtocol, self).__init__(*args, **kwargs) - self.protocol_name = "ssl" - - -def verify_SSL_key_and_cert(keyfile, certfile): - """ - This function looks for RSA key and certificate in the current - directory. If files ssl.key and ssl.cert does not exist, they - are created. - """ - - if not (os.path.exists(keyfile) and os.path.exists(certfile)): - # key/cert does not exist. Create. - import subprocess - from Crypto.PublicKey import RSA - from twisted.conch.ssh.keys import Key - - print(" Creating SSL key and certificate ... ", end=' ') - - try: - # create the RSA key and store it. - KEY_LENGTH = 1024 - rsaKey = Key(RSA.generate(KEY_LENGTH)) - keyString = rsaKey.toString(type="OPENSSH") - file(keyfile, 'w+b').write(keyString) - except Exception as err: - print(NO_AUTOGEN.format(err=err, keyfile=keyfile)) - sys.exit(5) - - # try to create the certificate - CERT_EXPIRE = 365 * 20 # twenty years validity - # default: - # openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300 - exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (keyfile, certfile, CERT_EXPIRE) - try: - subprocess.call(exestring) - except OSError as err: - raise OSError(NO_AUTOCERT.format(err=err, certfile=certfile, keyfile=keyfile, exestring=exestring)) - print("done.") - - -def getSSLContext(): - """ - This is called by the portal when creating the SSL context - server-side. - - Returns: - ssl_context (tuple): A key and certificate that is either - existing previously or or created on the fly. - - """ - keyfile = os.path.join(_GAME_DIR, "server", "ssl.key") - certfile = os.path.join(_GAME_DIR, "server", "ssl.cert") - - verify_SSL_key_and_cert(keyfile, certfile) - return twisted_ssl.DefaultOpenSSLContextFactory(keyfile, certfile) diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 357b20c81b..1a7f9fa18e 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -43,8 +43,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ def __init__(self, *args, **kwargs): - self.protocol_name = "telnet" super(TelnetProtocol, self).__init__(*args, **kwargs) + self.protocol_key = "telnet" def connectionMade(self): """ @@ -58,8 +58,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # this number is counted down for every handshake that completes. # when it reaches 0 the portal/server syncs their data self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp - self.init_session(self.protocol_name, client_address, self.factory.sessionhandler) + self.init_session(self.protocol_key, client_address, self.factory.sessionhandler) self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else 'utf-8' # add this new connection to sessionhandler so # the Server becomes aware of it. diff --git a/evennia/server/portal/telnet_ssl.py b/evennia/server/portal/telnet_ssl.py new file mode 100644 index 0000000000..cbeb108ac5 --- /dev/null +++ b/evennia/server/portal/telnet_ssl.py @@ -0,0 +1,146 @@ +""" +This allows for running the telnet communication over an encrypted SSL tunnel. To use it, requires a +client supporting Telnet SSL. + +The protocol will try to automatically create the private key and certificate on the server side +when starting and will warn if this was not possible. These will appear as files ssl.key and +ssl.cert in mygame/server/. + +""" +from __future__ import print_function + +import os +try: + from OpenSSL import crypto + from twisted.internet import ssl as twisted_ssl +except ImportError as error: + errstr = """ + {err} + Telnet-SSL requires the PyOpenSSL library and dependencies: + + pip install pyopenssl pycrypto enum pyasn1 service_identity + + Stop and start Evennia again. If no certificate can be generated, you'll + get a suggestion for a (linux) command to generate this locally. + + """ + raise ImportError(errstr.format(err=error)) + +from django.conf import settings +from evennia.server.portal.telnet import TelnetProtocol + +_GAME_DIR = settings.GAME_DIR + +_PRIVATE_KEY_LENGTH = 2048 +_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl.key") +_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl-public.key") +_CERTIFICATE_FILE = os.path.join(_GAME_DIR, "server", "ssl.cert") +_CERTIFICATE_EXPIRE = 365 * 24 * 60 * 60 * 20 # 20 years +_CERTIFICATE_ISSUER = {"C": "EV", "ST": "Evennia", "L": "Evennia", "O": + "Evennia Security", "OU": "Evennia Department", "CN": "evennia"} + +# messages + +NO_AUTOGEN = """ +Evennia could not auto-generate the SSL private- and public keys ({{err}}). +If this error persists, create them manually (using the tools for your OS). The files +should be placed and named like this: + {} + {} +""".format(_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE) + +NO_AUTOCERT = """ +Evennia's could not auto-generate the SSL certificate ({{err}}). +The private key already exists here: + {} +If this error persists, create the certificate manually (using the private key and +the tools for your OS). The file should be placed and named like this: + {} +""".format(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE) + + +class SSLProtocol(TelnetProtocol): + """ + Communication is the same as telnet, except data transfer + is done with encryption set up by the portal at start time. + """ + + def __init__(self, *args, **kwargs): + super(SSLProtocol, self).__init__(*args, **kwargs) + self.protocol_key = "telnet/ssl" + + +def verify_or_create_SSL_key_and_cert(keyfile, certfile): + """ + Verify or create new key/certificate files. + + Args: + keyfile (str): Path to ssl.key file. + certfile (str): Parth to ssl.cert file. + + Notes: + If files don't already exist, they are created. + + """ + + if not (os.path.exists(keyfile) and os.path.exists(certfile)): + # key/cert does not exist. Create. + try: + # generate the keypair + keypair = crypto.PKey() + keypair.generate_key(crypto.TYPE_RSA, _PRIVATE_KEY_LENGTH) + + with open(_PRIVATE_KEY_FILE, 'wt') as pfile: + pfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair)) + print("Created SSL private key in '{}'.".format(_PRIVATE_KEY_FILE)) + + with open(_PUBLIC_KEY_FILE, 'wt') as pfile: + pfile.write(crypto.dump_publickey(crypto.FILETYPE_PEM, keypair)) + print("Created SSL public key in '{}'.".format(_PUBLIC_KEY_FILE)) + + except Exception as err: + print(NO_AUTOGEN.format(err=err)) + return False + + else: + + try: + # create certificate + cert = crypto.X509() + subj = cert.get_subject() + for key, value in _CERTIFICATE_ISSUER.items(): + setattr(subj, key, value) + cert.set_issuer(subj) + + cert.set_serial_number(1000) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(_CERTIFICATE_EXPIRE) + cert.set_pubkey(keypair) + cert.sign(keypair, 'sha1') + + with open(_CERTIFICATE_FILE, 'wt') as cfile: + cfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + print("Created SSL certificate in '{}'.".format(_CERTIFICATE_FILE)) + + except Exception as err: + print(NO_AUTOCERT.format(err=err)) + return False + + return True + + +def getSSLContext(): + """ + This is called by the portal when creating the SSL context + server-side. + + Returns: + ssl_context (tuple): A key and certificate that is either + existing previously or created on the fly. + + """ + + if verify_or_create_SSL_key_and_cert(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE): + return twisted_ssl.DefaultOpenSSLContextFactory(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE) + else: + return None diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index d025b85018..70ebcab1ec 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -31,6 +31,9 @@ class WebSocketClient(Protocol, Session): """ Implements the server-side of the Websocket connection. """ + def __init__(self, *args, **kwargs): + super(WebSocketClient, self).__init__(*args, **kwargs) + self.protocol_key = "webclient/websocket" def connectionMade(self): """ diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py index c31f5d8eab..aa774e265c 100644 --- a/evennia/server/portal/webclient_ajax.py +++ b/evennia/server/portal/webclient_ajax.py @@ -298,7 +298,7 @@ class AjaxWebClientSession(session.Session): """ def __init__(self, *args, **kwargs): - self.protocol_name = "ajax/comet" + self.protocol_key = "webclient/ajax" super(AjaxWebClientSession, self).__init__(*args, **kwargs) def get_client_session(self): diff --git a/evennia/server/session.py b/evennia/server/session.py index 5b707187d3..eb77321a24 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -46,8 +46,8 @@ class Session(object): a new session is established. Args: - protocol_key (str): By default, one of 'telnet', 'ssh', - 'ssl' or 'web'. + protocol_key (str): By default, one of 'telnet', 'telnet/ssl', 'ssh', + 'webclient/websocket' or 'webclient/ajax'. address (str): Client address. sessionhandler (SessionHandler): Reference to the main sessionhandler instance.