conch_client.xhtml   [plain text]


<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Writing a client with Twisted.Conch</title>
  </head>

  <body>
    <h1>Writing a client with Twisted.Conch</h1>

    <h2>Introduction</h2>

<p>In the original days of computing, rsh/rlogin were used to connect to
remote computers and execute commands. These commands had the problem
that the passwords and commands were sent in the clear. To solve this
problem, the SSH protocol was created. Twisted.Conch implements the
second version of this protocol.</p>

    <h2>Writing a client</h2>

<p>Writing a client with Conch involves sub-classing 4 classes: <code
class="API">twisted.conch.ssh.transport.SSHClientTransport</code>, <code
class="API">twisted.conch.ssh.userauth.SSHUserAuthClient</code>, <code
class="API">twisted.conch.ssh.connection.SSHConnection</code>, and <code
class="API">twisted.conch.ssh.channel.SSHChannel</code>. We'll start out
with <code class="python">SSHClientTransport</code> because it's the base 
of the client.</p>

<h2>The Transport</h2>

<pre class="python">
from twisted.conch import error
from twisted.conch.ssh import transport
from twisted.internet import defer

class ClientTransport(transport.SSHClientTransport):

    def verifyHostKey(self, pubKey, fingerprint):
        if fingerprint != 'b1:94:6a:c9:24:92:d2:34:7c:62:35:b4:d2:61:11:84':
            return defer.fail(error.ConchError('bad key'))
        else:
            return defer.succeed(1)

    def connectionSecure(self):
        self.requestService(ClientUserAuth('user', ClientConnection()))
</pre>

<p>See how easy it is? <code class="python">SSHClientTransport</code>
handles the negotiation of encryption and the verification of keys
for you. The one security element that you as a client writer need to
implement is <code class="python">verifyHostKey()</code>. This method
is called with two strings: the public key sent by the server and its
fingerprint. You should verify the host key the server sends, either
by checking against a hard-coded value as in the example, or by asking
the user. <code class="python">verifyHostKey</code> returns a <code
class="API">twisted.internet.defer.Deferred</code> which gets a callback
if the host key is valid, or an errback if it is not. Note that in the
above, replace 'user' with the username you're attempting to ssh with,
for instance a call to <code class="python">os.getlogin()</code> for the
current user.</p>

<p>The second method you need to implement is <code
class="python">connectionSecure()</code>. It is called when the
encryption is set up and other services can be run. The example requests
that the <code class="python">ClientUserAuth</code> service be started.
This service will be discussed next.</p>

<h2>The Authorization Client</h2>

<pre class="python">
from twisted.conch.ssh import keys, userauth

# these are the public/private keys from test_conch

publicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3\
/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHR\
ivcJSkbh/C+BR3utDS555mV'

privateKey = """-----BEGIN RSA PRIVATE KEY-----
MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW
4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw
vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb
Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1
xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8
PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2
gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu
DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML
pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP
EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg==
-----END RSA PRIVATE KEY-----"""

class ClientUserAuth(userauth.SSHUserAuthClient):

    def getPassword(self, prompt = None):
        return 
        # this says we won't do password authentication

    def getPublicKey(self):
        return keys.getPublicKeyString(data = publicKey)

    def getPrivateKey(self):
        return defer.succeed(keys.getPrivateKeyObject(data = privateKey))
</pre>

<p>Again, fairly simple. The <code
class="python">SSHUserAuthClient</code> takes care of most
of the work, but the actual authentication data needs to be
supplied. <code class="python">getPassword()</code> asks for a
password, <code class="python">getPublicKey()</code> and <code
class="python">getPrivateKey()</code> get public and private keys,
respectively. <code class="python">getPassword()</code> returns
a <code class="python">Deferred</code> that is called back with
the password to use. <code class="python">getPublicKey()</code>
returns the SSH key data for the public key to use. <code
class="python">keys.getPublicKeyString()</code> will take
keys in OpenSSH and LSH format, and convert them to the
required format. <code class="python">getPrivateKey()</code>
returns a <code class="python">Deferred</code> which is
called back with the key object (as used in PyCrypto) for
the private key. <code class="python">getPassword()</code>
and <code class="python">getPrivateKey()</code> return <code
class="python">Deferreds</code> because they may need to ask the user
for input.</p>

<p>Once the authentication is complete, <code
class="python">SSHUserAuthClient</code> takes care of starting the code
<code class="python">SSHConnection</code> object given to it. Next, we'll
look at how to use the <code class="python">SSHConnection</code></p>

<h2>The Connection</h2>

<pre class="python">
from twisted.conch.ssh import connection

class ClientConnection(connection.SSHConnection):

    def serviceStarted(self):
        self.openChannel(CatChannel(conn = self))
</pre>

<p><code class="python">SSHConnection</code> is the easiest,
as it's only responsible for starting the channels. It has
other methods, those will be examined when we look at <code
class="python">SSHChannel</code>.</p>

<h2>The Channel</h2>

<pre class="python">
from twisted.conch.ssh import channel, common

class CatChannel(channel.SSHChannel):

    name = 'session'

    def channelOpen(self, data):
        d = self.conn.sendRequest(self, 'exec', common.NS('cat'),
                                  wantReply = 1)
        d.addCallback(self._cbSendRequest)
        self.catData = ''

    def _cbSendRequest(self, ignored):
        self.write('This data will be echoed back to us by "cat."\r\n')
        self.conn.sendEOF(self)
        self.loseConnection()

    def dataReceived(self, data):
        self.catData += data

    def closed(self):
        print 'We got this from "cat":', self.catData
</pre>

<p>Now that we've spent all this time getting the server and
client connected, here is where that work pays off. <code
class="python">SSHChannel</code> is the interface between you and the
other side. This particular channel opens a session and plays with the
'cat' program, but your channel can implement anything, so long as the
server supports it.</p>

<p>The <code class="python">channelOpen()</code> method is
where everything gets started. It gets passed a chunk of data;
however, this chunk is usually nothing and can be ignored.
Our <code class="python">channelOpen()</code> initializes our
channel, and sends a request to the other side, using the
<code class="python">sendRequest()</code> method of the <code
class="python">SSHConnection</code> object. Requests are used to send
events to the other side. We pass the method self so that it knows to
send the request for this channel. The 2nd argument of 'exec' tells the
server that we want to execute a command. The third argument is the data
that accompanies the request. <code class="API">common.NS</code> encodes
the data as a length-prefixed string, which is how the server expects
the data. We also say that we want a reply saying that the process has a
been started. <code class="python">sendRequest()</code> then returns a
<code class="python">Deferred</code> which we add a callback for.</p>

<p>Once the callback fires, we send the data. <code
class="python">SSHChannel</code> supports the <code class="API">
twisted.internet.interface.Transport</code> interface, so
it can be given to Protocols to run them over the secure
connection. In our case, we just write the data directly. <code
class="python">sendEOF()</code> does not follow the interface,
but Conch uses it to tell the other side that we will write no
more data. <code class="python">loseConnection()</code> shuts
down our side of the connection, but we will still receive data
through <code class="python">dataReceived()</code>. The <code
class="python">closed()</code> method is called when both sides of the
connection are closed, and we use it to display the data we received
(which should be the same as the data we sent.)</p>

<p>Finally, let's actually invoke the code we've set up.</p>

<h2>The main() function</h2>
<pre class="python">
from twisted.internet import protocol, reactor

def main():
    factory = protocol.ClientFactory()
    factory.protocol = ClientTransport
    reactor.connectTCP('localhost', 22, factory)
    reactor.run()

if __name__ == "__main__":
    main()
</pre>

<P>We call <code class="python">connectTCP()</code> to connect to
localhost, port 22 (the standard port for ssh), and pass it an instance
of <code class="API">twisted.internet.protocol.ClientFactory</code>.
This instance has the attribute <code class="python">protocol</code>
set to our earlier <code class="python">ClientTransport</code>
class. Note that the protocol is set to the class <code
class="python">ClientFactory</code>, not an instance of
<code class="python">ClientFactory</code>! When the <code
class="python">connectTCP</code> call completes, the protocol will be
called to create a <code class="python">ClientTransport()</code> object
- this then invokes all our previous work.</P>

<P>It's worth noting that in the example <code class="python">main()</code> 
routine, the <code class="python">reactor.run()</code> call never returns. 
If you want to make the program exit, call 
<code class="python">reactor.stop()</code> in the earlier 
<code class="python">closed()</code> method.</P>

<P>If you wish to observe the interactions in more detail, adding a call
to <code class="python">log.startLogging(sys.stdout, setStdout=0)</code>
before the <code class="python">reactor.run()</code> call will send all
logging to stdout.</P>

</body></html>