SSH with Twisted - Setting Up a Custom SSH Server continued (
Page 2 of 5 )
sshserver.py will run an SSH server on port 2222. Connect to this server with an SSH client using the username admin and password aaa, and try typing some commands:
$ ssh admin@localhost -p 2222
admin@localhost's password: aaa
>>> 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 from
twisted.conch.recvline.HistoricRecvline
.
HistoricRecvLine
is 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.recvline
also provides a plain
RecvLine
class that works the same way, but without the command history.
The
lineReceived
method in
HistoricRecvLine
is 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 between
HistoricRecvLine
and a regular
Protocol
, which come from the fact that with
HistoricRecvLine
you’re actually manipulating the current contents of a user’s terminal window, rather than just printing out text. To print a line of output, use
self.terminal.write
; to go to the next line, use
self.nextLine
.
The
twisted.conch.avatar.ConchUser
class 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 implement
twisted.conch.interfaces.ISession
. The
SSHDemoAvatar
class in Example 10-1 doesn’t actually implement all of
ISession
; it only implements enough for the user to get a shell. The
openShell
method will be called with a
twisted.conch.ssh.session. SSHSessionProcessProtocol
object 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 a
twisted.conch.insults.insults.ServerProtocol
object. You can pass extra arguments to
insults.ServerProtocol
, and it will use them to initialize your protocol object. This sets up your protocol to use a virtual terminal. Then use
makeConnection
to connect the two protocols to each other. The client’s protocol actually expects
makeConnection
to be called with a an object implementing the lower-level
twisted.internet.interfaces.ITransport
interface, not a
Protocol
; the
twisted.conch.session.wrapProtocol
function wraps a
Protocol
in a minimal
ITransport
interface.
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 a
requestAvatar
method. The SSH server will call
requestAvatar
with the username as
avatarId
and
twisted.conch.interfaces.IAvatar
as one of the interfaces. Return your subclass of
twisted.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 the
Crypto.PublicKey.RSA
module to generate these keys.
RSA.generate
takes a key length as the first argument and an entropy-generating function as the second argument; the
twisted.conch.ssh.common
module provides the
entropy.get_bytes
function for this purpose.
RSA.generate
returns a
Crypto.PublicKey.RSA.RSAobj
object. You extract public and private key strings from the
RSAobj
by passing it to the
getPublicKeyString
and
getPrivateKeyString
functions from the
twisted.conch.ssh.keys
module. 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 call
RSA.generate
after your program has entered the Twisted event loop.
RSA.generate
is 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 its
portal
attribute to a portal using your realm, and register a credentials checker that can handle
twisted.cred.credentials.IUsernamePassword
credentials. Set the
SSHFactory
’s
publicKeys
attribute to a dictionary that matches encryption algorithms to key string objects. To get the RSA key string object, pass your public key as the
data
keyword to
keys.getPublicKeyString
. Then set the
privateKeys
attribute to a dictionary that matches protocols to key objects. To get the RSA private key object, pass your private key as the
data
keyword to
keys.getPrivateKey
. Both
getPublicKeyString
and
getPrivateKey
can take a filename keyword instead, to load a key directly from a file. Once the
SSHFactory
has the keys, it’s ready to go. Call
reactor.listenTCP
to have it start listening on a port and you’ve got an SSH server.