>>> Welcome to my test SSH server. Commands: clear echo help quit whoami $ whoami admin $ help echo Echo a string. Usage: echo my line of text $ echo hello SSH world! hello SSH world! $ quit
Connection to localhost closed.
If you’ve already been using an SSH server on your local machine, you might get an error when you try to connect to the server in this example. You’ll get a message saying something like “Remote host identification has changed” or “Host key verification failed,” and your SSH client will refuse to connect.
The reason you get this error message is that your SSH client is remembering the public key used by your regular localhost SSH server. The server in Example 10-1 has its own key, and when the client sees that the keys are different, it gets suspicious that this new server may be an impostor pretending to be your regular localhost SSH server. To fix this problem, edit your ~/.ssh/known_hosts file (or wherever your SSH client keeps its list of recognized servers) and remove the localhost entry.
How Does That Work?
The SSHDemoProtocol class in Example 10-1 inherits fromtwisted.conch.recvline.HistoricRecvline.HistoricRecvLineis a protocol with built-in features for building command-line shells. It gives your shell features that most people take for granted in a modern shell, including backspacing, the ability to use the arrow keys to move the cursor forwards and backwards on the current line, and a command history that can be accessed using the up and down arrows.twisted.conch.recvlinealso provides a plainRecvLineclass that works the same way, but without the command history.
ThelineReceivedmethod inHistoricRecvLineis called whenever a user enters a line. Example 10-1 shows how you might override this method to parse and execute commands. There are a couple of differences betweenHistoricRecvLineand a regularProtocol, which come from the fact that withHistoricRecvLineyou’re actually manipulating the current contents of a user’s terminal window, rather than just printing out text. To print a line of output, useself.terminal.write; to go to the next line, useself.nextLine.
Thetwisted.conch.avatar.ConchUserclass represents the actions available to an authenticated SSH user. By default,ConchUser doesn’t allow the client to do anything. To make it possible for the user to get a shell, make his avatar implementtwisted.conch.interfaces.ISession. TheSSHDemoAvatarclass in Example 10-1 doesn’t actually implement all ofISession; it only implements enough for the user to get a shell. TheopenShellmethod will be called with atwisted.conch.ssh.session. SSHSessionProcessProtocolobject that represents the encrypted client’s end of the encrypted channel. You have to perform a few steps to connect the client’s protocol to your shell protocol so they can communicate with each other. First, wrap your protocol class in atwisted.conch.insults.insults.ServerProtocolobject. You can pass extra arguments toinsults.ServerProtocol, and it will use them to initialize your protocol object. This sets up your protocol to use a virtual terminal. Then usemakeConnectionto connect the two protocols to each other. The client’s protocol actually expectsmakeConnectionto be called with a an object implementing the lower-leveltwisted.internet.interfaces.ITransportinterface, not aProtocol; thetwisted.conch.session.wrapProtocolfunction wraps aProtocolin a minimalITransportinterface.
The library traditionally used for manipulating a Unix terminal is called curses. So the Twisted developers, never willing to pass up the chance to use a pun in a module name, chose the name insults for this library of classes for terminal programming.
To make a realm for your SSH server, write a class that has arequestAvatar method. The SSH server will callrequestAvatarwith the username asavatarIdandtwisted.conch.interfaces.IAvataras one of the interfaces. Return your subclass oftwisted.conch. avatar.ConchUser.
There’s only one more thing you’ll need to have a complete SSH server: a unique set of public and private keys. Example 10-1 demonstrates how you can use theCrypto.PublicKey.RSAmodule to generate these keys.RSA.generatetakes a key length as the first argument and an entropy-generating function as the second argument; thetwisted.conch.ssh.commonmodule provides theentropy.get_bytesfunction for this purpose.RSA.generatereturns aCrypto.PublicKey.RSA.RSAobjobject. You extract public and private key strings from theRSAobj by passing it to thegetPublicKeyStringandgetPrivateKeyStringfunctions from thetwisted.conch.ssh.keysmodule. Example 10-1 saves its keys to disk after generating them the first time it runs: you need to keep these keys preserved between clients so clients can identify and trust your sever.
Note that you wouldn’t want to callRSA.generateafter your program has entered the Twisted event loop.RSA.generateis a blocking function that can take quite some time to complete.
To run the SSH server, create a twisted.conch.ssh.factory.SSHFactory object. Set itsportalattribute to a portal using your realm, and register a credentials checker that can handletwisted.cred.credentials.IUsernamePasswordcredentials. Set theSSHFactory’spublicKeysattribute to a dictionary that matches encryption algorithms to key string objects. To get the RSA key string object, pass your public key as thedatakeyword tokeys.getPublicKeyString. Then set theprivateKeysattribute to a dictionary that matches protocols to key objects. To get the RSA private key object, pass your private key as thedata keyword tokeys.getPrivateKey. BothgetPublicKeyString andgetPrivateKeycan take a filename keyword instead, to load a key directly from a file. Once theSSHFactoryhas the keys, it’s ready to go. Callreactor.listenTCPto have it start listening on a port and you’ve got an SSH server.