Running Commands on a Remote Server
This lab demonstrates how to write an SSH client. You can use twisted.conch to communicate with a server using SSH: logging in, executing commands, and capturing the output.
10.4.1. How Do I Do That?
There are several classes that work together to make up a twisted.conch.ssh SSH client. The TRansport.SSHClientTransport class sets up the connection and verifies the identity of the server. The userauth.SSHUserAuthClient logs in using your authentication credentials. The connection.SSHConnection class takes over once you've logged in, and creates one or more channel.SSHChannel objects, which you then use to communicate with the server over a secure channel. Example 10-4 shows how you can use these classes to make an SSH client that logs into a server, runs a command, and prints the output.
Example 10-4. sshclient.py
from twisted.conch import error from twisted.conch.ssh import transport, connection, keys, userauth, channel, common from twisted.internet import defer, protocol, reactor class ClientCommandTransport(transport.SSHClientTransport): def _ _init_ _(self, username, password, command): self.username = username self.password = password self.command = command def verifyHostKey(self, pubKey, fingerprint): # in a real app, you should verify that the fingerprint matches # the one you expected to get from this server return defer.succeed(True) def connectionSecure(self): self.requestService( PasswordAuth(self.username, self.password, ClientConnection(self.command))) class PasswordAuth(userauth.SSHUserAuthClient): def _ _init_ _(self, user, password, connection): userauth.SSHUserAuthClient._ _init_ _(self, user, connection) self.password = password def getPassword(self, prompt=None): return defer.succeed(self.password) class ClientConnection(connection.SSHConnection): def _ _init_ _(self, cmd, *args, **kwargs): connection.SSHConnection._ _init_ _(self) self.command = cmd def serviceStarted(self): self.openChannel(CommandChannel(self.command, conn=self)) class CommandChannel(channel.SSHChannel): name = 'session' def _ _init_ _(self, command, *args, **kwargs): channel.SSHChannel._ _init_ _(self, *args, **kwargs) self.command = command def channelOpen(self, data): self.conn.sendRequest( self, 'exec', common.NS(self.command), wantReply=True).addCallback( self._gotResponse) def _gotResponse(self, _): self.conn.sendEOF(self) def dataReceived(self, data): print data def closed(self): reactor.stop( ) class ClientCommandFactory(protocol.ClientFactory): def _ _init_ _(self, username, password, command): self.username = username self.password = password self.command = command def buildProtocol(self, addr): protocol = ClientCommandTransport( self.username, self.password, self.command) return protocol if __name__ == "_ _main_ _": import sys, getpass server = sys.argv[1] command = sys.argv[2] username = raw_input("Username: ") password = getpass.getpass("Password: ") factory = ClientCommandFactory(username, password, command) reactor.connectTCP(server, 22, factory) reactor.run( )
Run sshclient.py with two arguments: a hostname and a command. It will ask for your username and password, log into the server, execute the command, and print the output. For example, you could run the who command to get a list of who's currently logged in to the server:
$ python sshclient.py myserver.example.com who Username: abe Password: password root pts/0 Jun 11 21:35 (192.168.0.13) phil pts/2 Jun 22 13:58 (192.168.0.1) phil pts/3 Jun 22 13:58 (192.168.0.1)
10.4.2. How Does That Work?
The ClientCommandTransport in Example 10-4 handles the initial connection to the SSH server. Its verifyHostKey method checks to make sure the server's public key matches your expectations. Typically, you'd remember each server the first time you connected, and then check on subsequent connections to make sure that another server wasn't maliciously trying to pass itself off as the server you expected. Here, it just returns a TRue value without bothering to check the key. The connectionSecure method is called as soon as the initial encrypted connection has been established. This is the appropriate time to send your login credentials, by passing a userauth.SSHUserAuthClient to self.requestService, along with a connection.SSHConnection object that should manage the connection after authentication succeeds.
The PasswordAuth inherits from userauth.SSHUserAuthClient. It has to implement only a single method, getPassword, which returns the password it will use to log in. If you wanted to use public key authentication, you'd implement the methods getPublicKey and getPrivateKey instead, returning the appropriate key as a string in each case.
The ClientConnection class in Example 10-4 will have its serviceStarted method called as soon as the client has successfully logged in. It calls self.openChannel with a CommandChannel object, which is a subclass of channel.SSHChannel. This object is used to work with an authenticated channel to the SSH server. Its channelOpen method is called when the channel is ready. At this point, you can call self.conn.sendRequest to send a command to the server. You have to encode data sent over SSH as a specially formatted network string; to get a string in this format, pass it to the twisted.conch.common.NS function. Set the keyword argument wantReply to TRue if you're interested in getting a response from the command; this setting will cause sendRequest to return a Deferred that will be called back when the command is completed. (If you don't set wantReply to true, sendRequest will return None.) As data is received from the server, it will be passed to dataReceived. Once you're done using the channel, close it by calling self.conn.sendEOF. The closed method will be called to let you know when the channel has been successfully closed.