diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index e8c310cae..3b658d54f 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -10,14 +10,11 @@ by game/evennia.py). from __future__ import print_function from builtins import object -import time import sys import os from twisted.application import internet, service from twisted.internet import protocol, reactor -from twisted.internet.task import LoopingCall -from twisted.web import server import django django.setup() from django.conf import settings @@ -228,9 +225,9 @@ if TELNET_ENABLED: if SSL_ENABLED: - # Start SSL game connection (requires PyOpenSSL). + # 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 = "" @@ -241,15 +238,20 @@ if SSL_ENABLED: factory = protocol.ServerFactory() 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 - print(" ssl%s: %s" % (ifacestr, port)) + 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) + + print(" ssl%s: %s" % (ifacestr, port)) + else: + print(" ssl%s: %s (deactivated - keys/certificate unset)" % (ifacestr, port)) if SSH_ENABLED: diff --git a/evennia/server/portal/ssl.py b/evennia/server/portal/ssl.py deleted file mode 100644 index 8b638ed23..000000000 --- a/evennia/server/portal/ssl.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -This is a simple context factory for auto-creating -SSL keys and certificates. - -""" -from __future__ import print_function - -import os -import sys -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 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_ssl.py b/evennia/server/portal/telnet_ssl.py new file mode 100644 index 000000000..b6869400a --- /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_name = "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(_PRIVATE_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(_PRIVATE_KEY_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