--- /dev/null
+"""A SSH Interface class.\r
+\r
+An interface to ssh on posix systems, and plink (part of the Putty\r
+suite) on Win32 systems.\r
+\r
+By Rasjid Wilcox.\r
+Copyright (c) 2002.\r
+\r
+Version: 0.2\r
+Last modified 4 September 2002.\r
+\r
+Drawing on ideas from work by Julian Schaefer-Jasinski, Guido's telnetlib and\r
+version 0.1 of pyssh (http://pyssh.sourceforge.net) by Chuck Esterbrook.\r
+\r
+Licenced under a Python 2.2 style license. See License.txt.\r
+"""\r
+\r
+DEBUG_LEVEL = 1\r
+\r
+import os, getpass\r
+import signal # should cause all KeyboardInterrupts to go to the main thread\r
+ # try for Linux, does not seem to be try under Cygwin\r
+import nbpipe\r
+import time\r
+\r
+# Constants\r
+SSH_PORT=806\r
+SSH_PATH=''\r
+\r
+CTRL_C=chr(3)\r
+\r
+READ_LAZY=0\r
+READ_SOME=1\r
+READ_ALL=2\r
+READ_UNTIL=3\r
+\r
+# set the path to ssh / plink, and chose the popen2 funciton to use\r
+if os.name=='posix':\r
+ import fssa # we can look for ssh-agent on posix\r
+ # XXX Can we on Win32/others?\r
+ import ptyext # if my patch gets accepted, change this to check for a\r
+ # sufficiently high version of python, and assign ptyext=pty\r
+ # if sufficient.\r
+ sshpopen2=ptyext.popen2\r
+ CLOSE_STR='~.'\r
+ tp=os.popen('/usr/bin/which ssh')\r
+ SSH_PATH=tp.read().strip()\r
+ try:\r
+ tp.close()\r
+ except IOError:\r
+ # probably no child process\r
+ pass\r
+ if SSH_PATH == '':\r
+ tp=os.popen('command -v ssh') # works in bash, ash etc, not csh etc.\r
+ SSH_PATH=tp.read().strip()\r
+ tp.close()\r
+ if SSH_PATH == '':\r
+ check = ['/usr/bin/ssh', '/usr/local/bin/ssh', '/bin/ssh']\r
+ for item in check:\r
+ if os.path.isfile(item):\r
+ SSH_PATH=item\r
+ break\r
+ PORT_STR='-p '\r
+else:\r
+ sshpopen2=os.popen2\r
+ CLOSE_STR=CTRL_C # FIX-ME: This does not work.\r
+ # I think I need to implement a 'kill' component\r
+ # to the close function using win32api.\r
+ SSH_PATH=''\r
+ PORT_STR='-P '\r
+\r
+class mysshError(Exception):\r
+ """Error class for myssh."""\r
+ pass\r
+\r
+# Helper functions\r
+def _prompt(prompt):\r
+ """Print the message as the prompt for input.\r
+ Return the text entered."""\r
+ noecho = (prompt.lower().find('password:') >= 0) or \\r
+ (prompt.lower().find('passphrase:') >=0)\r
+ print """User input required for ssh connection.\r
+ (Type Ctrl-C to abort connection.)"""\r
+ abort = 0\r
+ try:\r
+ if noecho:\r
+ response = getpass.getpass(prompt)\r
+ else:\r
+ response = raw_input(prompt)\r
+ except KeyboardInterrupt:\r
+ response = ''\r
+ abort = 1\r
+ return response, abort\r
+\r
+class Ssh:\r
+ """A SSH connection class."""\r
+ def __init__(self, username=None, host='localhost', port=None):\r
+ """Constructor. This does not try to connect."""\r
+ self.debuglevel = DEBUG_LEVEL\r
+ self.sshpath = SSH_PATH\r
+ self.username = username\r
+ self.host = host\r
+ self.port = port\r
+ self.isopen = 0\r
+ self.sshpid = 0 # perhaps merge this with isopen\r
+ #self.old_handler = signal.getsignal(signal.SIGCHLD)\r
+ #sig_handler = signal.signal(signal.SIGCHLD, self.sig_handler)\r
+ \r
+ def __del__(self):\r
+ """Destructor -- close the connection."""\r
+ if self.isopen:\r
+ self.close()\r
+ \r
+ def sig_handler(self, signum, stack):\r
+ """ Handle SIGCHLD signal """\r
+ if signum == signal.SIGCHLD:\r
+ try:\r
+ os.waitpid(self.sshpid, 0)\r
+ except:\r
+ pass\r
+ if self.old_handler != signal.SIG_DFL:\r
+ self.old_handler(signum, stack)\r
+\r
+ def attach_agent(self, key=None):\r
+ if os.name != 'posix':\r
+ # only posix support at this time\r
+ return\r
+ if 'SSH_AUTH_SOCK' not in os.environ.keys():\r
+ fssa.fssa(key)\r
+\r
+ def set_debuglevel(self, debuglevel):\r
+ """Set the debug level."""\r
+ self.debuglevel = debuglevel\r
+ \r
+ def set_sshpath(self, sshpath):\r
+ """Set the ssh path."""\r
+ self.sshpath=sshpath\r
+ \r
+ # Low level functions\r
+ def open(self, cmd=None):\r
+ """Opens a ssh connection.\r
+ \r
+ Raises an mysshError if myssh.sshpath is not a file.\r
+ Raises an error if attempting to open an already open connection.\r
+ """\r
+ #self.attach_agent()\r
+\r
+ if not os.path.isfile(self.sshpath):\r
+ raise mysshError, \\r
+ "Path to ssh or plink is not defined or invalid.\nsshpath='%s'" \\r
+ % self.sshpath\r
+ if self.isopen:\r
+ raise mysshError, "Connection already open."\r
+ sshargs = ''\r
+ if self.sshpath.lower().find('plink') != -1:\r
+ sshargs = '-ssh '\r
+ if self.port and self.port != '':\r
+ sshargs += PORT_STR + self.port + ' '\r
+ sshargs += " -o StrictHostKeyChecking=no -o PasswordAuthentication=yes -o PubkeyAuthentication=no "\r
+ if self.username and self.username !='':\r
+ sshargs += self.username + '@'\r
+ sshargs += self.host\r
+ if cmd:\r
+ sshargs += ' ' + cmd\r
+ if self.debuglevel:\r
+ print ">> Running %s %s." % (self.sshpath, sshargs)\r
+ # temporary workaround until I get pid's working under win32\r
+ #print sshargs\r
+ if os.name == 'posix':\r
+ self.sshin, self.sshoutblocking, self.sshpid = \\r
+ sshpopen2(self.sshpath + ' ' + sshargs)\r
+ else:\r
+ self.sshin, self.sshoutblocking = \\r
+ sshpopen2(self.sshpath + ' ' + sshargs)\r
+ self.sshout = nbpipe.nbpipe(self.sshoutblocking)\r
+ self.isopen = 1\r
+ if self.debuglevel:\r
+ print ">> ssh pid is %s." % self.sshpid\r
+ \r
+ def close(self, addnewline=1):\r
+ """Close the ssh connection by closing the input and output pipes.\r
+ Returns the closing messages.\r
+ \r
+ On Posix systems, by default it adds a newline before sending the\r
+ disconnect escape sequence. Turn this off by setting addnewline=0.\r
+ """\r
+ if os.name == 'posix':\r
+ try:\r
+ if addnewline:\r
+ self.write('\n')\r
+ self.write(CLOSE_STR)\r
+ except (OSError, IOError, mysshError):\r
+ pass\r
+ output = self.read_lazy()\r
+ try:\r
+ self.sshin.close()\r
+ self.sshoutblocking.close()\r
+ except:\r
+ pass\r
+ if os.name == 'posix':\r
+ try:\r
+ os.kill(self.sshpid, signal.SIGHUP)\r
+ os.waitpid(self.sshpid, 0)\r
+ except:\r
+ pass\r
+ self.isopen = 0\r
+ if self.debuglevel:\r
+ print ">> Connection closed."\r
+ return output\r
+ \r
+ def write(self, text):\r
+ """Send text to the ssh process."""\r
+ # May block?? Probably not in practice, as ssh has a large input buffer.\r
+ if self.debuglevel:\r
+ print ">> Sending %s" % text\r
+ if self.isopen:\r
+ while len(text):\r
+ numtaken = os.write(self.sshin.fileno(),text)\r
+ if self.debuglevel:\r
+ print ">> %s characters taken" % numtaken\r
+ text = text[numtaken:]\r
+ else:\r
+ raise mysshError, "Attempted to write to closed connection."\r
+ \r
+ # There is a question about what to do with connections closed by the other\r
+ # end. Should write and read check for this, and force proper close?\r
+ def read_lazy(self):\r
+ """Lazy read from sshout. Waits a little, but does not block."""\r
+ return self.sshout.read_lazy()\r
+ \r
+ def read_some(self):\r
+ """Always read at least one block, unless the connection is closed.\r
+ My block."""\r
+ if self.isopen:\r
+ return self.sshout.read_some()\r
+ else:\r
+ return self.sshout.read_lazy()\r
+ \r
+ def read_until(self, match, timeout=None):\r
+ """Read until a given string is encountered or until timeout.\r
+\r
+ When no match is found, return whatever is available instead,\r
+ possibly the empty string. Raise EOFError if the connection\r
+ is closed and no cooked data is available.\r
+\r
+ """\r
+ if self.isopen:\r
+ return self.sshout.read_until(match, timeout)\r
+ else:\r
+ return self.sshout.read_lazy() \r
+\r
+ def read_all(self):\r
+ """Reads until end of file hit. May block."""\r
+ if self.isopen:\r
+ return self.sshout.read_all()\r
+ else:\r
+ return self.sshout.read_lazy() \r
+ \r
+ # High level funcitons\r
+ def login(self, logintext='Last login:', prompt_callback=_prompt):\r
+ """Logs in to the ssh host. Checks for standard prompts, and calls\r
+ the function passed as promptcb to process them.\r
+ Returns the login banner, or 'None' if login process aborted.\r
+ """\r
+ self.open()\r
+ banner = self.read_some()\r
+ if self.debuglevel:\r
+ print ">> 1st banner read is: %s" % banner\r
+ while banner.find(logintext) == -1:\r
+ response, abort = prompt_callback(banner)\r
+ if abort:\r
+ return self.close()\r
+ self.write(response + '\n')\r
+ banner = self.read_some()\r
+ return banner\r
+ \r
+ def logout(self):\r
+ """Logs out the session."""\r
+ self.close()\r
+ \r
+ def sendcmd(self, cmd, readtype=READ_SOME, expected=None, timeout=None):\r
+ """Sends the command 'cmd' over the ssh connection, and returns the\r
+ result. By default it uses read_some, which may block.\r
+ """\r
+ if cmd[-1] != '\n':\r
+ cmd += '\n'\r
+ self.write(cmd)\r
+ if readtype == READ_ALL:\r
+ return self.read_all()\r
+ elif readtype == READ_LAZY:\r
+ return self.read_lazy()\r
+ elif readtype == READ_UNTIL and expected is not None:\r
+ return self.read_until(expected, timeout)\r
+ else:\r
+ return self.read_some()\r
+ \r
+def test():\r
+ """Test routine for myssh.\r
+ \r
+ Usage: python myssh.py [-d] [-sshp path-to-ssh] [username@host | host] [port]\r
+ \r
+ Default host is localhost, default port is 22.\r
+ """\r
+ import sys\r
+ debug = 0\r
+ if sys.argv[1:] and sys.argv[1] == '-d':\r
+ debug = 1\r
+ del sys.argv[1]\r
+ testsshpath = SSH_PATH\r
+ if sys.argv[1:] and sys.argv[1] == '-sshp':\r
+ testsshpath = sys.argv[2]\r
+ del sys.argv[1]\r
+ del sys.argv[1]\r
+ testusername = None\r
+ testhost = 'localhost'\r
+ testport = '22'\r
+ if sys.argv[1:]:\r
+ testhost = sys.argv[1]\r
+ if testhost.find('@') != -1:\r
+ testusername, testhost = testhost.split('@')\r
+ if sys.argv[2:]:\r
+ testport = sys.argv[2]\r
+ \r
+ testcon = Ssh(testusername, testhost, testport)\r
+ testcon.set_debuglevel(debug)\r
+ testcon.set_sshpath(testsshpath)\r
+ testcon.login()\r
+ \r
+ cmd = None\r
+ while (cmd != 'exit') and testcon.isopen:\r
+ cmd = raw_input("Enter command to send: ")\r
+ print testcon.sendcmd(cmd)\r
+ testcon.close()\r
+\r
+if __name__ == '__main__':\r
+ test()\r