Sunday, April 10, 2011

Twisted Conch in 60 Seconds: PTY Requests

Greetings and welcome once again to Twisted Conch in 60 Seconds, a series introducing SSH development with Twisted. If you're just joining me, you probably want to go back to the beginning before reading this article.

The previous example constructed a server which could present some data to clients which managed to authenticate successfully. However, it also produced an error message which I didn't explain:

PTY allocation request failed on channel 0

This is an error message the client software displays (so you may see something different depending on which SSH client you use - I am using OpenSSH 5.1). What the client is trying to tell us is that it asked the server to allocate a PTY - a pseudo-terminal - and the server would not do it. The client doesn't think this is a fatal error though, so it continues on to request a shell, which it gets.

It turns out that PTY requests are another very common channel request (that is, a request that is scoped to a particular channel, as the shell request is). Almost any time you expect clients to show up at your server and request a shell, it is somewhat likely that they'll request a PTY as well, so you probably want to handle this case.

Recall how the shell request was handled in the previous example:

class SimpleSession(SSHChannel):
name = 'session'

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


As a PTY request is just another type of channel request, I can extend this class with a new method, request_pty_req, in order to handle this additional request type.

    def request_pty_req(self, data):


I didn't talk about the data parameter last time because the shell request makes no use of it. However, a PTY request does use it. The client packs some information about its capabilities into a particular string format and that's what this method will receive as an argument. Conch provides a method for parsing this string into a more manageable form:

        self.terminalName, self.windowSize, modes = parseRequest_pty_req(data)


terminalName is a string giving the name of the terminal. This is typically the value of the TERM environment variable on the client - which might not be what you expect, and is often simply set to "xterm".

More useful than the terminal name is the windowSize tuple. This tuple has four elements. The first two give the number of rows and columns available on the client. The third and fourth give the terminal width and height in pixels. For many clients, the latter isn't available or useful so these values are set to 0.

The last value, modes, is rather complicated. You can read about it in RFC 4254, section 8. Or you can take my word for it for now that you probably don't care about it, at least for now.

Notice that I saved some of that data. I'm going to use it to make request_shell a little more interesting. Before moving on to that, there's one last thing to do in request_pty_req though. I need to indicate that the PTY request was successful:

        return True


Now, here's a new version of request_shell which uses those two new attributes:

    def request_shell(self, data):
self.write(
"Your terminal name is %r. "
"Your terminal is %d columns wide and %d rows tall." % (
self.terminalName, self.windowSize[0], self.windowSize[1]))
self.loseConnection()
return True


Now when clients connect to the server, they will be able to see what terminal they're running and how big it is. Here's the full code listing for this version of the example, including a new import for parseRequest_pty_req:

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
from twisted.conch.ssh.session import parseRequest_pty_req

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_pty_req(self, data):
self.terminalName, self.windowSize, modes = parseRequest_pty_req(data)
return True

def request_shell(self, data):
self.write(
"Your terminal name is %r.\r\n"
"Your terminal is %d columns wide and %d rows tall.\r\n" % (
self.terminalName, self.windowSize[1], self.windowSize[0]))
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()

Compared to the last example, only the methods of SimpleSession have changed. If you run this server, connect to it, and authenticate then you'll be rewarded with a little information about your terminal. Or, if your SSH client does not request a PTY for some reason (for example, if you pass -T to the OpenSSH client) then you'll see an error when the shell request fails, since terminalName and windowSize won't have been set.

At this point, I have explained enough of Conch that you could almost write a very simple application with it. For example, you could add an SSH server to your Twisted Web server which sends request logs to the client. This would just involve calling the write repeatedly, once for each request which was handled. However, for many applications, you may actually want to accept input from the client. Stick around for the next article in which I'll cover precisely that.

No comments:

Post a Comment