Monday, August 1, 2011

Twisted Conch in 60 Seconds: Protocols

Welcome once more to Twisted Conch in 60 Seconds, the tutorial series about writing SSH applications with Twisted.

Over the past several articles, I've introduced the APIs for letting clients establish a new logical connection to your SSH server, generating output on those connections, accepting input on those connections, and detecting the end of those connections. Taken together, these four activities map almost exactly onto the standard Twisted protocol abstraction (represented and documented by the IProtocol interface). In this article, I'll show you how to use any IProtocol implementation from Twisted to interact with an SSH channel.

The previous example implemented an echo-ish type of server by customizing the session. I'll duplicate that functionality here, but in an IProtocol implementation:

class EchoProtocol(Protocol):
def connectionMade(self):
self.transport.write("Echo protocol connected\r\n")

def dataReceived(self, bytes):
self.transport.write("echo: " + repr(bytes) + "\r\n")

def connectionLost(self, reason):
print 'Connection lost', reason


Each of the three events a protocol might receive is handled here - connection made and data received are handled by writing something to the connection, and connection lost is handled by writing something to stdout (writing to the connection is no longer an option after it has been lost, of course). This is a simple, fairly typical Twisted-based protocol implementation; you'll find protocols like this all over the place.
The major part of the requirements for using this is having a transport to which to connect it. The SSHSession class you're now familiar with can serve in just that role. Only a little code to put it together with a protocol is required. Again I'll create a "session" channel which accepts but ignores pty requests:

class SimpleSession(SSHSession):
name = 'session'

def request_pty_req(self, data):
return True


And again I'll override request_shell so that the protocol is connected to the channel/transport as soon as the client requests a shell.

    def request_shell(self, data):
protocol = EchoProtocol()


All I've done so far here is make an instance of the protocol defined above. Next I'll create the transport object and hook it up to this protocol. This part uses a couple helpers from twisted.conch.ssh.session, SSHSessionProcessProtocol and wrapProtocol:

        transport = SSHSessionProcessProtocol(self)
protocol.makeConnection(transport)
transport.makeConnection(wrapProtocol(protocol))
self.client = transport
return True


Each step here is necessary, but the specifics aren't very interesting or important. Suffice it to say it hooks objects up as necessary so that bytes can be passed from the SSHSession to the protocol and vice versa.

The rest of the code for this version is the same as it has been in the previous versions (except for new imports). Here's the full code listing:

from twisted.internet.protocol import Protocol
from twisted.cred.portal import Portal
from twisted.cred.checkers import FilePasswordDB
from twisted.conch.ssh.factory import SSHFactory
from twisted.internet import reactor
from twisted.conch.ssh.keys import Key
from twisted.conch.interfaces import IConchUser
from twisted.conch.avatar import ConchUser
from twisted.conch.ssh.session import (
SSHSession, SSHSessionProcessProtocol, wrapProtocol)

with open('id_rsa') as privateBlobFile:
privateBlob = privateBlobFile.read()
privateKey = Key.fromString(data=privateBlob)

with open('id_rsa.pub') as publicBlobFile:
publicBlob = publicBlobFile.read()
publicKey = Key.fromString(data=publicBlob)

class EchoProtocol(Protocol):
def connectionMade(self):
self.transport.write("Echo protocol connected\r\n")

def dataReceived(self, bytes):
self.transport.write("echo: " + repr(bytes) + "\r\n")

def connectionLost(self, reason):
print 'Connection lost', reason

def nothing():
pass

class SimpleSession(SSHSession):
name = 'session'

def request_pty_req(self, data):
return True

def request_shell(self, data):
protocol = EchoProtocol()
transport = SSHSessionProcessProtocol(self)
protocol.makeConnection(transport)
transport.makeConnection(wrapProtocol(protocol))
self.client = transport
return True

class SimpleRealm(object):
def requestAvatar(self, avatarId, mind, *interfaces):
user = ConchUser()
user.channelLookup['session'] = SimpleSession
return IConchUser, user, nothing

factory = SSHFactory()
factory.privateKeys = {'ssh-rsa': privateKey}
factory.publicKeys = {'ssh-rsa': publicKey}
factory.portal = Portal(SimpleRealm())
factory.portal.registerChecker(FilePasswordDB("ssh-passwords"))

reactor.listenTCP(2022, factory)
reactor.run()


Connect to this server with an ssh client and type away, you should see something like this:


exarkun@boson:~$ ssh -p 2022 localhost
exarkun@localhost's password:
Echo protocol connected
echo: 'h'
echo: 'e'
echo: 'l'
echo: 'l'
echo: 'o'
echo: ','
echo: ' '
echo: 'w'
echo: 'o'
echo: 'r'
echo: 'l'
echo: 'd'
echo: '.'

Tune in next time for a surprise topic!

No comments:

Post a Comment