Welcome back to Twisted Conch in 60 Seconds, the documentation series about writing SSH servers (and eventually, clients) with Twisted. In earlier entries, I’ve covered some of the basics of accepting client connections and generating output. In this edition, I’ll cover accepting input from the client.
Recall that in the previous two example programs, a SSHChannel
subclass was responsible for sending some output to the client connection. The same object is going to have input from the client delivered to it. Some of you may not even be surprised to learn that the way this is done is that the channel has its dataReceived
method called with a string:
class SimpleSession(SSHChannel):
def dataReceived(self, bytes):
self.write("echo: " + repr(bytes) + "\r\n")
The single argument to dataReceived
, bytes
, is a str
containing the bytes sent from the client. This simple implementation of dataReceived
escapes the received data with repr
so it’s easy to see what bytes were actually received and then sends them back with a little formatting. As you might expect, dataReceived
is being passed bytes from a reliable, ordered, stream-oriented connection. That is, it’s a lot like TCP. This means you need to be careful about message boundaries, possibly buffering up several calls with of data before handling it. Unlike TCP, of course, these bytes were sent encrypted over the network. This is an SSH tutorial, after all!
Aside from this method, it’s still necessary to acknowledge the PTY request the client will send:
def request_pty_req(self, data):
return True
But since this example doesn’t make use of the terminal name, size, or mode information all the method needs to do is return True to indicate that the request was successful. Similarly, the shell request must be allowed:
def request_shell(self, data):
return True
Again, nothing going on here except a positive acknowledgement of the request so the client will be happy and move on. That’s all of the code that’s changed since the last example. The full code listing looks like this:
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_pty_req(self, data):
return True
def request_shell(self, data):
return True
def dataReceived(self, bytes):
self.write("echo: " + repr(bytes) + "\r\n")
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()
Et voilà, a custom SSH server which accepts input and generates output. Next time, the exciting topic of detecting EOF on that input stream…