From 9c7326da88b1e748142a6e99c7cd769c585c84f5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 27 Jan 2018 15:35:27 +0100 Subject: [PATCH 1/4] Fix output error in ssh protocol. Resolves #1427 --- evennia/server/portal/ssh.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index e993e74bc3..6ac61e7026 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -66,6 +66,7 @@ class SshProtocol(Manhole, session.Session): here. """ + noisy = False def __init__(self, starttuple): """ @@ -104,7 +105,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): @@ -228,7 +229,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): @@ -302,6 +303,9 @@ class SshProtocol(Manhole, session.Session): class ExtraInfoAuthServer(SSHUserAuthServer): + + noisy = False + def auth_password(self, packet): """ Password authentication. @@ -327,6 +331,7 @@ class AccountDBPasswordChecker(object): useful for the Realm. """ + noisy = False credentialInterfaces = (credentials.IUsernamePassword,) def __init__(self, factory): @@ -362,6 +367,8 @@ class PassAvatarIdTerminalRealm(TerminalRealm): """ + noisy = False + def _getAvatar(self, avatarId): comp = components.Componentized() user = self.userFactory(comp, avatarId) @@ -383,6 +390,8 @@ class TerminalSessionTransport_getPeer(object): """ + noisy = False + def __init__(self, proto, chainedProtocol, avatar, width, height): self.proto = proto self.avatar = avatar @@ -440,7 +449,6 @@ 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") From a2549410a8079a134b2c53b6c758bd88f8a77551 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 27 Jan 2018 19:50:50 +0100 Subject: [PATCH 2/4] Cleanup of telnet-ssl, creating public/private/certs in code rather than calling openssl in a subprocess. Also better handle errors and reporting. --- evennia/server/portal/portal.py | 28 +++--- evennia/server/portal/ssl.py | 115 ---------------------- evennia/server/portal/telnet_ssl.py | 146 ++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 128 deletions(-) delete mode 100644 evennia/server/portal/ssl.py create mode 100644 evennia/server/portal/telnet_ssl.py diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index e8c310cae1..3b658d54fb 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 8b638ed23d..0000000000 --- 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 0000000000..b6869400a1 --- /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 From 37a5a67391d66743462fc411366bf30e9dbed889 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 27 Jan 2018 21:30:10 +0100 Subject: [PATCH 3/4] Clean up SSH key generation and output. --- evennia/server/portal/ssh.py | 48 +++++++++++++++++------------ evennia/server/portal/telnet_ssl.py | 6 ++-- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index 6ac61e7026..9534a67244 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) + class SshProtocol(Manhole, session.Session): """ @@ -426,32 +439,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'], @@ -466,14 +479,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/telnet_ssl.py b/evennia/server/portal/telnet_ssl.py index b6869400a1..b3d2ae4fa6 100644 --- a/evennia/server/portal/telnet_ssl.py +++ b/evennia/server/portal/telnet_ssl.py @@ -33,7 +33,7 @@ _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") +_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": @@ -96,7 +96,7 @@ def verify_or_create_SSL_key_and_cert(keyfile, certfile): 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)) + print("Created SSL public key in '{}'.".format(_PUBLIC_KEY_FILE)) except Exception as err: print(NO_AUTOGEN.format(err=err)) @@ -120,7 +120,7 @@ def verify_or_create_SSL_key_and_cert(keyfile, certfile): 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)) + print("Created SSL certificate in '{}'.".format(_CERTIFICATE_FILE)) except Exception as err: print(NO_AUTOCERT.format(err=err)) From e31b2a1ee4bb0c1ca89432e0fec4d5910b739334 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 27 Jan 2018 21:54:46 +0100 Subject: [PATCH 4/4] Rename protocol_keys ssl->telnet/ssl, websocket->webclient/websocket, ajax/comet->webclient/ajax. --- evennia/server/portal/ssh.py | 1 + evennia/server/portal/telnet.py | 4 ++-- evennia/server/portal/telnet_ssl.py | 2 +- evennia/server/portal/webclient.py | 3 +++ evennia/server/portal/webclient_ajax.py | 2 +- evennia/server/session.py | 4 ++-- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/evennia/server/portal/ssh.py b/evennia/server/portal/ssh.py index 9534a67244..29bd3eac87 100644 --- a/evennia/server/portal/ssh.py +++ b/evennia/server/portal/ssh.py @@ -90,6 +90,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] diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index bdb937bde5..aef96b6f38 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -34,8 +34,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): """ @@ -49,7 +49,7 @@ 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) # add this new connection to sessionhandler so # the Server becomes aware of it. self.sessionhandler.connect(self) diff --git a/evennia/server/portal/telnet_ssl.py b/evennia/server/portal/telnet_ssl.py index b3d2ae4fa6..cbeb108ac5 100644 --- a/evennia/server/portal/telnet_ssl.py +++ b/evennia/server/portal/telnet_ssl.py @@ -67,7 +67,7 @@ class SSLProtocol(TelnetProtocol): def __init__(self, *args, **kwargs): super(SSLProtocol, self).__init__(*args, **kwargs) - self.protocol_name = "ssl" + self.protocol_key = "telnet/ssl" def verify_or_create_SSL_key_and_cert(keyfile, certfile): 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 ed7dabbe8f..093a2c0d7a 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -47,8 +47,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.