""" This module implements the ssh (Secure SHell) protocol for encrypted connections. 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 from twisted.cred.checkers import credentials from twisted.cred.portal import Portal from twisted.conch.ssh.keys import Key from twisted.conch.interfaces import IConchUser from twisted.conch.ssh.userauth import SSHUserAuthServer from twisted.conch.ssh import common from twisted.conch.insults import insults from twisted.conch.manhole_ssh import TerminalRealm, _Glue, ConchFactory from twisted.conch.manhole import Manhole, recvline from twisted.internet import defer from twisted.conch import interfaces as iconch from twisted.python import components from django.conf import settings from src.server import session from src.players.models import PlayerDB from src.utils import ansi, utils, logger ENCODINGS = settings.ENCODINGS CTRL_C = '\x03' CTRL_D = '\x04' CTRL_BACKSLASH = '\x1c' CTRL_L = '\x0c' class SshProtocol(Manhole, session.Session): """ Each player connecting over ssh gets this protocol assigned to them. All communication between game and player goes through here. """ def __init__(self, starttuple): """ For setting up the player. If player is not None then we'll login automatically. """ self.authenticated_player = starttuple[0] self.cfactory = starttuple[1] # obs may not be called self.factory, it gets overwritten! def terminalSize(self, width, height): """ Initialize the terminal and connect to the new session. """ # Clear the previous input line, redraw it at the new # cursor position self.terminal.eraseDisplay() self.terminal.cursorHome() self.width = width self.height = height # initialize the session 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): """ This is called when the connection is first established. """ recvline.HistoricRecvLine.connectionMade(self) 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 # initalize def handle_INT(self): """ Handle ^C as an interrupt keystroke by resetting the current input variables to their initial state. """ self.lineBuffer = [] self.lineBufferIndex = 0 self.terminal.nextLine() self.terminal.write("KeyboardInterrupt") self.terminal.nextLine() def handle_EOF(self): """ Handles EOF generally used to exit. """ if self.lineBuffer: self.terminal.write('\a') else: self.handle_QUIT() def handle_FF(self): """ Handle a 'form feed' byte - generally used to request a screen refresh/redraw. """ self.terminal.eraseDisplay() self.terminal.cursorHome() def handle_QUIT(self): """ Quit, end, and lose the connection. """ self.terminal.loseConnection() def connectionLost(self, reason=None): """ This is executed when the connection is lost for whatever reason. It can also be called directly, from the disconnect method. """ insults.TerminalProtocol.connectionLost(self, reason) 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() 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. """ 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. """ for line in string.split('\n'): self.terminal.write(line) #this is the telnet-specific method for sending self.terminal.nextLine() # session-general method hooks def disconnect(self, reason="Connection closed. Goodbye for now."): """ Disconnect from server """ if reason: self.data_out(reason) self.connectionLost(reason) def data_out(self, text=None, **kwargs): """ Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings. ssh flags: raw=True - leave all ansi markup and tokens unparsed nomarkup=True - remove all ansi markup """ try: text = utils.to_str(text if text else "", encoding=self.encoding) except Exception, e: self.lineSend(str(e)) return raw = kwargs.get("raw", False) nomarkup = kwargs.get("nomarkup", False) if raw: self.lineSend(string) else: self.lineSend(ansi.parse_ansi(string.strip("{r") + "{r", strip_ansi=nomarkup)) class ExtraInfoAuthServer(SSHUserAuthServer): def auth_password(self, packet): """ Password authentication. Used mostly for setting up the transport so we can query username and password later. """ password = common.getNS(packet[1:])[0] c = credentials.UsernamePassword(self.user, password) c.transport = self.transport return self.portal.login(c, None, IConchUser).addErrback( self._ebPassword) class PlayerDBPasswordChecker(object): """ Checks the django db for the correct credentials for username/password otherwise it returns the player or None which is useful for the Realm. """ 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, self.factory) if player and player.user.check_password(password): res = (player, self.factory) return defer.succeed(res) class PassAvatarIdTerminalRealm(TerminalRealm): """ Returns an avatar that passes the avatarId through to the protocol. This is probably not the best way to do it. """ def _getAvatar(self, avatarId): comp = components.Componentized() user = self.userFactory(comp, avatarId) sess = self.sessionFactory(comp) sess.transportFactory = self.transportFactory sess.chainedProtocolFactory = lambda : self.chainedProtocolFactory(avatarId) comp.setComponent(iconch.IConchUser, user) comp.setComponent(iconch.ISession, sess) return user class TerminalSessionTransport_getPeer: """ Taken from twisted's TerminalSessionTransport which doesn't provide getPeer to the transport. This one does. """ def __init__(self, proto, chainedProtocol, avatar, width, height): self.proto = proto self.avatar = avatar self.chainedProtocol = chainedProtocol session = self.proto.session self.proto.makeConnection( _Glue(write=self.chainedProtocol.dataReceived, loseConnection=lambda: avatar.conn.sendClose(session), name="SSH Proto Transport")) def loseConnection(): self.proto.loseConnection() def getPeer(): session.conn.transport.transport.getPeer() self.chainedProtocol.makeConnection( _Glue(getPeer=getPeer, write=self.proto.write, loseConnection=loseConnection, name="Chained Proto Transport")) self.chainedProtocol.terminalProtocol.terminalSize(width, height) def getKeyPair(pubkeyfile, privkeyfile): """ This function looks for RSA keypair files in the current directory. If they do not exist, the keypair is created. """ if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)): # No keypair exists. Generate a new RSA keypair print " Generating SSH RSA keypair ...", 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") # save keys for the future. file(pubkeyfile, 'w+b').write(publicKeyString) file(privkeyfile, 'w+b').write(privateKeyString) print " done." else: publicKeyString = file(pubkeyfile).read() privateKeyString = file(privkeyfile).read() return Key.fromString(publicKeyString), Key.fromString(privateKeyString) def makeFactory(configdict): """ Creates the ssh server factory. """ pubkeyfile = "ssh-public.key" privkeyfile = "ssh-private.key" def chainProtocolFactory(username=None): return insults.ServerProtocol( configdict['protocolFactory'], *configdict.get('protocolConfigdict', (username,)), **configdict.get('protocolKwArgs', {})) rlm = PassAvatarIdTerminalRealm() rlm.transportFactory = TerminalSessionTransport_getPeer rlm.chainedProtocolFactory = chainProtocolFactory factory = ConchFactory(Portal(rlm)) factory.sessionhandler = configdict['sessions'] try: # create/get RSA keypair publicKey, privateKey = getKeyPair(pubkeyfile, privkeyfile) factory.publicKeys = {'ssh-rsa': publicKey} factory.privateKeys = {'ssh-rsa': privateKey} except Exception, e: 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)) return factory