From: Faiyaz Ahmed Date: Tue, 14 Nov 2006 19:36:09 +0000 (+0000) Subject: SSH and telnet library X-Git-Tag: Monitor-1.0-0~109 X-Git-Url: http://git.onelab.eu/?p=monitor.git;a=commitdiff_plain;h=ccae8f4040e579d2dc90c9d2c738ae3f7b357710 SSH and telnet library --- diff --git a/pyssh/__init__.py b/pyssh/__init__.py new file mode 100644 index 0000000..bc57610 --- /dev/null +++ b/pyssh/__init__.py @@ -0,0 +1,333 @@ +"""A SSH Interface class. + +An interface to ssh on posix systems, and plink (part of the Putty +suite) on Win32 systems. + +By Rasjid Wilcox. +Copyright (c) 2002. + +Version: 0.2 +Last modified 4 September 2002. + +Drawing on ideas from work by Julian Schaefer-Jasinski, Guido's telnetlib and +version 0.1 of pyssh (http://pyssh.sourceforge.net) by Chuck Esterbrook. + +Licenced under a Python 2.2 style license. See License.txt. +""" + +DEBUG_LEVEL = 0 + +import os, getpass +import signal # should cause all KeyboardInterrupts to go to the main thread + # try for Linux, does not seem to be try under Cygwin +import nbpipe +import time + +# Constants +SSH_PORT=806 +SSH_PATH='' + +CTRL_C=chr(3) + +READ_LAZY=0 +READ_SOME=1 +READ_ALL=2 +READ_UNTIL=3 + +# set the path to ssh / plink, and chose the popen2 funciton to use +if os.name=='posix': + import fssa # we can look for ssh-agent on posix + # XXX Can we on Win32/others? + import ptyext # if my patch gets accepted, change this to check for a + # sufficiently high version of python, and assign ptyext=pty + # if sufficient. + sshpopen2=ptyext.popen2 + CLOSE_STR='~.' + tp=os.popen('/usr/bin/which ssh') + SSH_PATH=tp.read().strip() + try: + tp.close() + except IOError: + # probably no child process + pass + if SSH_PATH == '': + tp=os.popen('command -v ssh') # works in bash, ash etc, not csh etc. + SSH_PATH=tp.read().strip() + tp.close() + if SSH_PATH == '': + check = ['/usr/bin/ssh', '/usr/local/bin/ssh', '/bin/ssh'] + for item in check: + if os.path.isfile(item): + SSH_PATH=item + break + PORT_STR='-p ' +else: + sshpopen2=os.popen2 + CLOSE_STR=CTRL_C # FIX-ME: This does not work. + # I think I need to implement a 'kill' component + # to the close function using win32api. + SSH_PATH='' + PORT_STR='-P ' + +class mysshError(Exception): + """Error class for myssh.""" + pass + +# Helper functions +def _prompt(prompt): + """Print the message as the prompt for input. + Return the text entered.""" + noecho = (prompt.lower().find('password:') >= 0) or \ + (prompt.lower().find('passphrase:') >=0) + print """User input required for ssh connection. + (Type Ctrl-C to abort connection.)""" + abort = 0 + try: + if noecho: + response = getpass.getpass(prompt) + else: + response = raw_input(prompt) + except KeyboardInterrupt: + response = '' + abort = 1 + return response, abort + +class Ssh: + """A SSH connection class.""" + def __init__(self, username=None, host='localhost', port=None): + """Constructor. This does not try to connect.""" + self.debuglevel = DEBUG_LEVEL + self.sshpath = SSH_PATH + self.username = username + self.host = host + self.port = port + self.isopen = 0 + self.sshpid = 0 # perhaps merge this with isopen + self.old_handler = signal.getsignal(signal.SIGCHLD) + sig_handler = signal.signal(signal.SIGCHLD, self.sig_handler) + + def __del__(self): + """Destructor -- close the connection.""" + if self.isopen: + self.close() + + def sig_handler(self, signum, stack): + """ Handle SIGCHLD signal """ + if signum == signal.SIGCHLD: + try: + os.waitpid(self.sshpid, 0) + except: + pass + if self.old_handler != signal.SIG_DFL: + self.old_handler(signum, stack) + + def attach_agent(self, key=None): + if os.name != 'posix': + # only posix support at this time + return + if 'SSH_AUTH_SOCK' not in os.environ.keys(): + fssa.fssa(key) + + def set_debuglevel(self, debuglevel): + """Set the debug level.""" + self.debuglevel = debuglevel + + def set_sshpath(self, sshpath): + """Set the ssh path.""" + self.sshpath=sshpath + + # Low level functions + def open(self, cmd=None): + """Opens a ssh connection. + + Raises an mysshError if myssh.sshpath is not a file. + Raises an error if attempting to open an already open connection. + """ + self.attach_agent() + if not os.path.isfile(self.sshpath): + raise mysshError, \ + "Path to ssh or plink is not defined or invalid.\nsshpath='%s'" \ + % self.sshpath + if self.isopen: + raise mysshError, "Connection already open." + sshargs = '' + if self.sshpath.lower().find('plink') != -1: + sshargs = '-ssh ' + if self.port and self.port != '': + sshargs += PORT_STR + self.port + ' ' + if self.username and self.username !='': + sshargs += self.username + '@' + sshargs += self.host + if cmd: + sshargs += ' ' + cmd + if self.debuglevel: + print ">> Running %s %s." % (self.sshpath, sshargs) + # temporary workaround until I get pid's working under win32 + if os.name == 'posix': + self.sshin, self.sshoutblocking, self.sshpid = \ + sshpopen2(self.sshpath + ' ' + sshargs) + else: + self.sshin, self.sshoutblocking = \ + sshpopen2(self.sshpath + ' ' + sshargs) + self.sshout = nbpipe.nbpipe(self.sshoutblocking) + self.isopen = 1 + if self.debuglevel: + print ">> ssh pid is %s." % self.sshpid + + def close(self, addnewline=1): + """Close the ssh connection by closing the input and output pipes. + Returns the closing messages. + + On Posix systems, by default it adds a newline before sending the + disconnect escape sequence. Turn this off by setting addnewline=0. + """ + if os.name == 'posix': + try: + if addnewline: + self.write('\n') + self.write(CLOSE_STR) + except (OSError, IOError, mysshError): + pass + output = self.read_lazy() + try: + self.sshin.close() + self.sshoutblocking.close() + except: + pass + if os.name == 'posix': + try: + os.kill(self.sshpid, signal.SIGHUP) + os.waitpid(self.sshpid, 0) + except: + pass + self.isopen = 0 + if self.debuglevel: + print ">> Connection closed." + return output + + def write(self, text): + """Send text to the ssh process.""" + # May block?? Probably not in practice, as ssh has a large input buffer. + if self.debuglevel: + print ">> Sending %s" % text + if self.isopen: + while len(text): + numtaken = os.write(self.sshin.fileno(),text) + if self.debuglevel: + print ">> %s characters taken" % numtaken + text = text[numtaken:] + else: + raise mysshError, "Attempted to write to closed connection." + + # There is a question about what to do with connections closed by the other + # end. Should write and read check for this, and force proper close? + def read_lazy(self): + """Lazy read from sshout. Waits a little, but does not block.""" + return self.sshout.read_lazy() + + def read_some(self): + """Always read at least one block, unless the connection is closed. + My block.""" + if self.isopen: + return self.sshout.read_some() + else: + return self.sshout.read_lazy() + + def read_until(self, match, timeout=None): + """Read until a given string is encountered or until timeout. + + When no match is found, return whatever is available instead, + possibly the empty string. Raise EOFError if the connection + is closed and no cooked data is available. + + """ + if self.isopen: + return self.sshout.read_until(match, timeout) + else: + return self.sshout.read_lazy() + + def read_all(self): + """Reads until end of file hit. May block.""" + if self.isopen: + return self.sshout.read_all() + else: + return self.sshout.read_lazy() + + # High level funcitons + def login(self, logintext='Last login:', prompt_callback=_prompt): + """Logs in to the ssh host. Checks for standard prompts, and calls + the function passed as promptcb to process them. + Returns the login banner, or 'None' if login process aborted. + """ + self.open() + banner = self.read_some() + if self.debuglevel: + print ">> 1st banner read is: %s" % banner + while banner.find(logintext) == -1: + response, abort = prompt_callback(banner) + if abort: + return self.close() + self.write(response + '\n') + banner = self.read_some() + return banner + + def logout(self): + """Logs out the session.""" + self.close() + + def sendcmd(self, cmd, readtype=READ_SOME, expected=None, timeout=None): + """Sends the command 'cmd' over the ssh connection, and returns the + result. By default it uses read_some, which may block. + """ + if cmd[-1] != '\n': + cmd += '\n' + self.write(cmd) + if readtype == READ_ALL: + return self.read_all() + elif readtype == READ_LAZY: + return self.read_lazy() + elif readtype == READ_UNTIL and expected is not None: + return self.read_until(expected, timeout) + else: + return self.read_some() + +def test(): + """Test routine for myssh. + + Usage: python myssh.py [-d] [-sshp path-to-ssh] [username@host | host] [port] + + Default host is localhost, default port is 22. + """ + import sys + debug = 0 + if sys.argv[1:] and sys.argv[1] == '-d': + debug = 1 + del sys.argv[1] + testsshpath = SSH_PATH + if sys.argv[1:] and sys.argv[1] == '-sshp': + testsshpath = sys.argv[2] + del sys.argv[1] + del sys.argv[1] + testusername = None + testhost = 'localhost' + testport = '22' + if sys.argv[1:]: + testhost = sys.argv[1] + if testhost.find('@') != -1: + testusername, testhost = testhost.split('@') + if sys.argv[2:]: + testport = sys.argv[2] + + testcon = Ssh(testusername, testhost, testport) + testcon.set_debuglevel(debug) + testcon.set_sshpath(testsshpath) + testcon.login() + + cmd = None + while (cmd != 'exit') and testcon.isopen: + cmd = raw_input("Enter command to send: ") + print testcon.sendcmd(cmd) + testcon.close() + +if __name__ == '__main__': + test() diff --git a/pyssh/fssa.py b/pyssh/fssa.py new file mode 100644 index 0000000..6ca17be --- /dev/null +++ b/pyssh/fssa.py @@ -0,0 +1,75 @@ +# vi:et:ts=4:tw=0 +""" fssa.py + + Search for an ssh-agent for the calling user and attach to it + if found. + + Tested on poxix only +""" +# This is a Python port of Steve Allen's fsa.sh script found +# at http://www.ucolick.org/~sla/ssh/sshcron.html +# Ported by Mark W. Alexander + +import os +if os.name == 'posix': + import pwd, stat, sys + from commands import getoutput + def fssa(key=None): + """ fssa(key=None) + + Searches /tmp/ssh-* owned by the calling user for ssh-agent + sockets. If key is provided, only sockets matching the key will be + considered. If key is not provided, the calling users username + will be used instead. + """ + user, pw, uid, gid, gecos, home, shell = pwd.getpwuid(os.getuid()) + if key is None: + key = user + + # Find /tmp/ssh-* dirs owned by this user + candidate_dirs=[] + for filename in os.listdir('/tmp'): + file_stat = os.stat("/tmp/%s" % (filename)) + if file_stat[stat.ST_UID] == os.getuid() \ + and stat.S_ISDIR(file_stat[stat.ST_MODE]) \ + and filename.find('ssh-') == 0: + candidate_dirs.append("/tmp/%s" % filename) + + candidate_sockets=[] + for d in candidate_dirs: + for f in os.listdir(d): + file_stat = os.stat("%s/%s" % (d, f)) + if file_stat[stat.ST_UID] == os.getuid() \ + and stat.S_ISSOCK(file_stat[stat.ST_MODE]) \ + and f.find('agent.') == 0: + candidate_sockets.append("%s/%s" % (d, f)) + alive = None + # order by pid, prefering sockets where the parent pid + # is gone. This gives preference to agents running in the + # background and reaped by init (maybe). Only works on + # systems with a /proc filesystem + if stat.S_ISDIR(os.stat("/proc")[stat.ST_MODE]): + reorder=[] + for s in candidate_sockets: + pid = s[s.find('.')+1:] + try: + stat.S_ISDIR(os.stat("/proc/%s" % pid)[stat.ST_MODE]) + reorder.append(s) + except: + reorder.insert(0,s) + candidate_sockets = reorder + + for s in candidate_sockets: + os.environ['SSH_AUTH_SOCK'] = s + try: + pubkey = getoutput("ssh-add -l 2>/dev/null") + except: + continue + if pubkey.find(key): + alive = 1 + break + os.environ.pop('SSH_AUTH_SOCK') + if alive: + return pubkey + else: + return None diff --git a/pyssh/nbpipe.py b/pyssh/nbpipe.py new file mode 100644 index 0000000..08b4f97 --- /dev/null +++ b/pyssh/nbpipe.py @@ -0,0 +1,111 @@ +"""Implements a non-blocking pipe class.""" + +# Since it uses thread rather than select, it is portable to at least +# posix and windows environments. + +# Author: Rasjid Wilcox, copyright (c) 2002 +# Ideas taken from the Python 2.2 telnetlib.py library. +# +# Last modified: 3 August 2002 +# Licence: Python 2.2 Style License. See license.txt. + +# TO DO: +# * Handle excpetions better, particularly Keyboard Interupts. +# * Possibly do a threadless version for posix environments +# where we can use select (is probably more efficient). +# * A test function. + +import Queue +import thread +import os +import time +import types + +#INT_TYPE = type(1) +MIN_TIMEOUT = 0.01 + +class nbpipe: + def __init__(self, readfile, pipesize=0, blocksize=1024): + """Initialise a non-blocking pipe object, given a real file or file-descriptor. + pipesize = the size (in blocks) of the queue used to buffer the blocks read + blocksize = the maximum block size for a raw read.""" + if type(readfile) == types.IntType: + self.fd = readfile + else: + self.fd = readfile.fileno() + self.pipesize = pipesize + self.blocksize = blocksize + self.eof = 0 + self._q = Queue.Queue(self.pipesize) + self.data = '' + thread.start_new_thread(self._readtoq, ()) + def _readtoq(self): + finish = 0 + while (1): + try: + item = os.read(self.fd, self.blocksize) + except (IOError, OSError): + finish = 1 + if (item == '') or finish: + # Wait until everything has been read from the queue before + # setting eof = 1 and exiting. + while not self._q.empty(): + time.sleep(MIN_TIMEOUT) + self.eof = 1 + thread.exit() + else: + self._q.put(item) + def has_data(self): + return self.data + def eof(self): + return self.eof + def read_lazy(self): + """Process and return data that's already in the queues (lazy). + + Return '' if no data available. Don't block. + + """ + while not self._q.empty(): + self.data += self._q.get() + data = self.data + self.data = '' + return data + def read_some(self, until_eof=False): + """Read at least one byte of cooked data unless EOF is hit. + + Return '' if EOF is hit. Block if no data is immediately + available. + + """ + data = '' + while (until_eof or not data) and not self.eof: + data += self.read_lazy() + time.sleep(MIN_TIMEOUT) + return data + def read_until(self, match, timeout=None): + """Read until a given string is encountered or until timeout. + + If no match is found or EOF is hit, return whatever is + available instead, possibly the empty string. + """ + if timeout is not None: + timeout = timeout / MIN_TIMEOUT + else: + timeout = 1 + n = len(match) + data = self.read_lazy() + i = 0 + while timeout >= 0 and not self.eof: + i = data.find(match, i) + if i >= 0: + i += n + self.data = data[i:] + return data[:i] + time.sleep(MIN_TIMEOUT) + timeout -= 1 + i = max(0, len(data) - n) + data += self.read_lazy() + return data + def read_all(self): + """Read until the EOF. May block.""" + return read_some(until_eof=True) diff --git a/pyssh/ptyext.py b/pyssh/ptyext.py new file mode 100644 index 0000000..b22835b --- /dev/null +++ b/pyssh/ptyext.py @@ -0,0 +1,209 @@ +"""Pseudo terminal utilities.""" + +# Bugs: No signal handling. Doesn't set slave termios and window size. +# Only tested on Linux. +# See: W. Richard Stevens. 1992. Advanced Programming in the +# UNIX Environment. Chapter 19. +# Author: Steen Lumholt -- with additions by Guido. + +from select import select, error +import os + +# Absurd: import termios and then delete it. This is to force an attempt +# to import pty to raise an ImportError on platforms that lack termios. +# Without this explicit import of termios here, some other module may +# import tty first, which in turn imports termios and dies with an +# ImportError then. But since tty *does* exist across platforms, that +# leaves a damaged module object for tty in sys.modules, and the import +# of tty here then appears to work despite that the tty imported is junk. +import termios +del termios + +import tty + +__all__ = ["openpty","fork","spawn","th_spawn","popen2"] + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +CHILD = 0 + +def openpty(): + """openpty() -> (master_fd, slave_fd) + Open a pty master/slave pair, using os.openpty() if possible.""" + + try: + return os.openpty() + except (AttributeError, OSError): + pass + master_fd, slave_name = _open_terminal() + slave_fd = slave_open(slave_name) + return master_fd, slave_fd + +def master_open(): + """master_open() -> (master_fd, slave_name) + Open a pty master and return the fd, and the filename of the slave end. + Deprecated, use openpty() instead.""" + + try: + master_fd, slave_fd = os.openpty() + except (AttributeError, OSError): + pass + else: + slave_name = os.ttyname(slave_fd) + os.close(slave_fd) + return master_fd, slave_name + + return _open_terminal() + +def _open_terminal(): + """Open pty master and return (master_fd, tty_name). + SGI and generic BSD version, for when openpty() fails.""" + try: + import sgi + except ImportError: + pass + else: + try: + tty_name, master_fd = sgi._getpty(os.O_RDWR, 0666, 0) + except IOError, msg: + raise os.error, msg + return master_fd, tty_name + for x in 'pqrstuvwxyzPQRST': + for y in '0123456789abcdef': + pty_name = '/dev/pty' + x + y + try: + fd = os.open(pty_name, os.O_RDWR) + except os.error: + continue + return (fd, '/dev/tty' + x + y) + raise os.error, 'out of pty devices' + +def slave_open(tty_name): + """slave_open(tty_name) -> slave_fd + Open the pty slave and acquire the controlling terminal, returning + opened filedescriptor. + Deprecated, use openpty() instead.""" + + return os.open(tty_name, os.O_RDWR) + +def fork(): + """fork() -> (pid, master_fd) + Fork and make the child a session leader with a controlling terminal.""" + + try: + pid, fd = os.forkpty() + except (AttributeError, OSError): + pass + else: + if pid == CHILD: + try: + os.setsid() + except OSError: + # os.forkpty() already set us session leader + pass + return pid, fd + + master_fd, slave_fd = openpty() + pid = os.fork() + if pid == CHILD: + # Establish a new session. + os.setsid() + os.close(master_fd) + + # Slave becomes stdin/stdout/stderr of child. + os.dup2(slave_fd, STDIN_FILENO) + os.dup2(slave_fd, STDOUT_FILENO) + os.dup2(slave_fd, STDERR_FILENO) + if (slave_fd > STDERR_FILENO): + os.close (slave_fd) + + # Parent and child process. + return pid, master_fd + +def _writen(fd, data): + """Write all the data to a descriptor.""" + while data != '': + n = os.write(fd, data) + data = data[n:] + +def _read(fd): + """Default read function.""" + return os.read(fd, 1024) + +def _copy(master_fd, master_read=_read, stdin_read=_read, stdin_fd=STDIN_FILENO, + stdout_fd=STDOUT_FILENO): + """Parent copy loop. + Copies + pty master -> stdout_fd (master_read) + stdin_fd -> pty master (stdin_read)""" + try: + mode = tty.tcgetattr(stdin_fd) + tty.setraw(stdin_fd) + restore = 1 + except tty.error: # This is the same as termios.error + restore = 0 + try: + while 1: + rfds, wfds, xfds = select( + [master_fd, stdin_fd], [], []) + if master_fd in rfds: + data = master_read(master_fd) + os.write(stdout_fd, data) + if stdin_fd in rfds: + data = stdin_read(stdin_fd) + _writen(master_fd, data) + except (IOError, OSError, error): # The last entry is select.error + if restore: + tty.tcsetattr(stdin_fd, tty.TCSAFLUSH, mode) + if stdin_fd > STDERR_FILENO: + os.close(stdin_fd) + if stdout_fd > STDERR_FILENO: + os.close(stdout_fd) + +def spawn(argv, master_read=_read, stdin_read=_read, stdin_fd=STDIN_FILENO, + stdout_fd=STDOUT_FILENO): + """Create a spawned process. The controlling terminal reads and + writes its data to stdin_fd and stdout_fd respectively. + + NOTE: This function does not return until one of the input or output file + descriptors are closed, or the child process exits.""" + if type(argv) == type(''): + argv = (argv,) + pid, master_fd = fork() + if pid == CHILD: + apply(os.execlp, (argv[0],) + argv) + _copy(master_fd, master_read, stdin_read, stdin_fd, stdout_fd) + +def th_spawn(argv, master_read=_read, stdin_read=_read, stdin_fd=STDIN_FILENO, + stdout_fd=STDOUT_FILENO): + """Create a spawned process. The controlling terminal reads and + writes its data to stdin_fd and stdout_fd respectively. The function + returns the pid of the spawned process. (It returns immediately.)""" + import thread + if type(argv) == type(''): + argv = (argv,) + pid, master_fd = fork() + if pid == CHILD: + apply(os.execlp, (argv[0],) + argv) + thread.start_new_thread(_copy, (master_fd, master_read, stdin_read, \ + stdin_fd, stdout_fd)) + return pid + +def popen2(cmd, bufsize=1024, master_read=_read, stdin_read=_read): + """Execute the shell command 'cmd' in a sub-process. + + If 'bufsize' is specified, it sets the buffer size for the I/O pipes. + The function returns (child_stdin, child_stdout, child_pid), where the + file objects are pipes connected to the spawned process's controling + terminal, and the child_pid is the pid of the child process. + """ + argv = ('/bin/sh', '-c', cmd) + child_stdin_rfd, child_stdin_wfd = os.pipe() + child_stdout_rfd, child_stdout_wfd = os.pipe() + child_pid = th_spawn(argv, master_read, stdin_read, child_stdin_rfd, \ + child_stdout_wfd) + child_stdin = os.fdopen(child_stdin_wfd, 'w', bufsize) + child_stdout = os.fdopen(child_stdout_rfd, 'r', bufsize) + return child_stdin, child_stdout, child_pid