Tuesday, April 5, 2011

Twisted Conch in 60 Seconds: A Trivial Channel

Welcome to the first nearly-useful edition of Twisted Conch in 60 Seconds. In previous posts, I demonstrated how to create an SSH server no one could log in to, and then how to create an SSH server with password authentication but no content beyond that. Today I'll show you how to present some data to clients which connect and successfully authenticate.

Recall that in the last example, I added SimpleRealm to the SSH server. The SimpleRealm was responsible for creating a user (or avatar) to handle authenticated connections. Since Conch provides a very simple user class, ConchUser, the realm was quite simple:

class SimpleRealm(object):
def requestAvatar(self, avatarId, mind, *interfaces):
return IConchUser, ConchUser(), nothing

Almost any behavior you might want to implement in a custom SSH server is going to go into the user object. For example, after an SSH client authenticates, in almost all cases the first thing it does is open a channel. SSH can multiplex many logical connections over a single TCP connection. Channels are the basis of this multiplexing. When an SSH client opens a channel, Conch turns this into a method call onto the user object, lookupChannel. Channels come in different types, but ConchUser doesn't define behavior for any of them by default. This explains the output of the previous example:

channel 0: open failed: unknown channel type: unknown channel

The channel I'm going to define in this example will be of the session type. This is the kind of channel almost all SSH clients request after authentication succeeds. Channels should typically subclass SSHChannel:

from twisted.conch.ssh.channel import SSHChannel

class SimpleSession(SSHChannel):

Channels also need to specify their type or name:

    name = 'session'

Aside from just passing uninterpreted bytes back and forth, as is done when you use SSH to get a remote shell, channels also support structured requests. SSHChannel lets you handle these requests by defining request_-prefixed methods. When a foo request is received, request_foo is called to handle it. One common request, and the only one I'm going to handle in this example, is a request for a shell. However, to keep things simple, I won't implement it to set up a real shell:

    def request_shell(self, data):
self.write("This session is very simple. Goodbye!\r\n")
self.loseConnection()
return True

Since the most common thing to use the command line ssh tool for is to get a remote shell, the client issues a shell request by default. After this, ssh is just passing bytes back and forth. It's up to the user and the shell process (such as /bin/bash) to make up and interpret those bytes.

However, this shell request handler does generate some bytes. It sends them to the client using the write method. Then it closes the channel (not the entire SSH connection) with the loseConnection method. Finally, it returns True to indicate that the request was successful.

The only thing left to do is make ConchUser capable of using SimpleSession to satisfy requests to open channels of type session. The channelLookup attribute of ConchUser makes it easy to do this. It's a dictionary the keys of which are channel types and the values of which are SSHChannel subclasses. This means SimpleRealm can be tweaked slightly to produce users which support session channels:

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

As before, we have a requestAvatar which returns a three-tuple of interface, avatar, and logout callable. However, this version of the realm also adds an item to the user's channelLookup dictionary before returning it. These users can therefore open new channels of type session.

Here's a complete code listing for this version of the example. Only SimpleSession is new, and only SimpleRealm.requestAvatar has changed.

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.channel import SSHChannel

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)

def nothing():
pass

class SimpleSession(SSHChannel):
name = 'session'

def request_shell(self, data):
self.write("This session is very simple. Goodbye!\r\n")
self.loseConnection()
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()

Run this server and connect to it (ssh -p 2022 localhost) and you should see output like this:

PTY allocation request failed on channel 0
This session is very simple. Goodbye!
Connection to localhost closed.

Next time I'll talk about what that first line of output means and ways to avoid it.

1 comment:

  1. Jean-Paul, This post is missing a `conch` tag. Thanks for the great explanations.

    ReplyDelete