1 """A SSH Interface class.
\r
3 An interface to ssh on posix systems, and plink (part of the Putty
\r
4 suite) on Win32 systems.
\r
10 Last modified 4 September 2002.
\r
12 Drawing on ideas from work by Julian Schaefer-Jasinski, Guido's telnetlib and
\r
13 version 0.1 of pyssh (http://pyssh.sourceforge.net) by Chuck Esterbrook.
\r
15 Licenced under a Python 2.2 style license. See License.txt.
\r
21 import signal # should cause all KeyboardInterrupts to go to the main thread
\r
22 # try for Linux, does not seem to be try under Cygwin
\r
37 # set the path to ssh / plink, and chose the popen2 funciton to use
\r
38 if os.name=='posix':
\r
39 import fssa # we can look for ssh-agent on posix
\r
40 # XXX Can we on Win32/others?
\r
41 import ptyext # if my patch gets accepted, change this to check for a
\r
42 # sufficiently high version of python, and assign ptyext=pty
\r
44 sshpopen2=ptyext.popen2
\r
46 tp=os.popen('/usr/bin/which ssh')
\r
47 SSH_PATH=tp.read().strip()
\r
51 # probably no child process
\r
54 tp=os.popen('command -v ssh') # works in bash, ash etc, not csh etc.
\r
55 SSH_PATH=tp.read().strip()
\r
58 check = ['/usr/bin/ssh', '/usr/local/bin/ssh', '/bin/ssh']
\r
60 if os.path.isfile(item):
\r
66 CLOSE_STR=CTRL_C # FIX-ME: This does not work.
\r
67 # I think I need to implement a 'kill' component
\r
68 # to the close function using win32api.
\r
72 class mysshError(Exception):
\r
73 """Error class for myssh."""
\r
77 def _prompt(prompt):
\r
78 """Print the message as the prompt for input.
\r
79 Return the text entered."""
\r
80 noecho = (prompt.lower().find('password:') >= 0) or \
\r
81 (prompt.lower().find('passphrase:') >=0)
\r
82 print """User input required for ssh connection.
\r
83 (Type Ctrl-C to abort connection.)"""
\r
87 response = getpass.getpass(prompt)
\r
89 response = raw_input(prompt)
\r
90 except KeyboardInterrupt:
\r
93 return response, abort
\r
96 """A SSH connection class."""
\r
97 def __init__(self, username=None, host='localhost', port=None):
\r
98 """Constructor. This does not try to connect."""
\r
99 self.debuglevel = DEBUG_LEVEL
\r
100 self.sshpath = SSH_PATH
\r
101 self.username = username
\r
105 self.sshpid = 0 # perhaps merge this with isopen
\r
106 self.old_handler = signal.getsignal(signal.SIGCHLD)
\r
107 sig_handler = signal.signal(signal.SIGCHLD, self.sig_handler)
\r
110 """Destructor -- close the connection."""
\r
114 def sig_handler(self, signum, stack):
\r
115 """ Handle SIGCHLD signal """
\r
116 if signum == signal.SIGCHLD:
\r
118 os.waitpid(self.sshpid, 0)
\r
121 if self.old_handler != signal.SIG_DFL:
\r
122 self.old_handler(signum, stack)
\r
124 def attach_agent(self, key=None):
\r
125 if os.name != 'posix':
\r
126 # only posix support at this time
\r
128 if 'SSH_AUTH_SOCK' not in os.environ.keys():
\r
131 def set_debuglevel(self, debuglevel):
\r
132 """Set the debug level."""
\r
133 self.debuglevel = debuglevel
\r
135 def set_sshpath(self, sshpath):
\r
136 """Set the ssh path."""
\r
137 self.sshpath=sshpath
\r
139 # Low level functions
\r
140 def open(self, cmd=None):
\r
141 """Opens a ssh connection.
\r
143 Raises an mysshError if myssh.sshpath is not a file.
\r
144 Raises an error if attempting to open an already open connection.
\r
146 self.attach_agent()
\r
147 if not os.path.isfile(self.sshpath):
\r
148 raise mysshError, \
\r
149 "Path to ssh or plink is not defined or invalid.\nsshpath='%s'" \
\r
152 raise mysshError, "Connection already open."
\r
154 if self.sshpath.lower().find('plink') != -1:
\r
156 if self.port and self.port != '':
\r
157 sshargs += PORT_STR + self.port + ' '
\r
158 if self.username and self.username !='':
\r
159 sshargs += self.username + '@'
\r
160 sshargs += self.host
\r
162 sshargs += ' ' + cmd
\r
163 if self.debuglevel:
\r
164 print ">> Running %s %s." % (self.sshpath, sshargs)
\r
165 # temporary workaround until I get pid's working under win32
\r
166 if os.name == 'posix':
\r
167 self.sshin, self.sshoutblocking, self.sshpid = \
\r
168 sshpopen2(self.sshpath + ' ' + sshargs)
\r
170 self.sshin, self.sshoutblocking = \
\r
171 sshpopen2(self.sshpath + ' ' + sshargs)
\r
172 self.sshout = nbpipe.nbpipe(self.sshoutblocking)
\r
174 if self.debuglevel:
\r
175 print ">> ssh pid is %s." % self.sshpid
\r
177 def close(self, addnewline=1):
\r
178 """Close the ssh connection by closing the input and output pipes.
\r
179 Returns the closing messages.
\r
181 On Posix systems, by default it adds a newline before sending the
\r
182 disconnect escape sequence. Turn this off by setting addnewline=0.
\r
184 if os.name == 'posix':
\r
188 self.write(CLOSE_STR)
\r
189 except (OSError, IOError, mysshError):
\r
191 output = self.read_lazy()
\r
194 self.sshoutblocking.close()
\r
197 if os.name == 'posix':
\r
199 os.kill(self.sshpid, signal.SIGHUP)
\r
200 os.waitpid(self.sshpid, 0)
\r
204 if self.debuglevel:
\r
205 print ">> Connection closed."
\r
208 def write(self, text):
\r
209 """Send text to the ssh process."""
\r
210 # May block?? Probably not in practice, as ssh has a large input buffer.
\r
211 if self.debuglevel:
\r
212 print ">> Sending %s" % text
\r
215 numtaken = os.write(self.sshin.fileno(),text)
\r
216 if self.debuglevel:
\r
217 print ">> %s characters taken" % numtaken
\r
218 text = text[numtaken:]
\r
220 raise mysshError, "Attempted to write to closed connection."
\r
222 # There is a question about what to do with connections closed by the other
\r
223 # end. Should write and read check for this, and force proper close?
\r
224 def read_lazy(self):
\r
225 """Lazy read from sshout. Waits a little, but does not block."""
\r
226 return self.sshout.read_lazy()
\r
228 def read_some(self):
\r
229 """Always read at least one block, unless the connection is closed.
\r
232 return self.sshout.read_some()
\r
234 return self.sshout.read_lazy()
\r
236 def read_until(self, match, timeout=None):
\r
237 """Read until a given string is encountered or until timeout.
\r
239 When no match is found, return whatever is available instead,
\r
240 possibly the empty string. Raise EOFError if the connection
\r
241 is closed and no cooked data is available.
\r
245 return self.sshout.read_until(match, timeout)
\r
247 return self.sshout.read_lazy()
\r
249 def read_all(self):
\r
250 """Reads until end of file hit. May block."""
\r
252 return self.sshout.read_all()
\r
254 return self.sshout.read_lazy()
\r
256 # High level funcitons
\r
257 def login(self, logintext='Last login:', prompt_callback=_prompt):
\r
258 """Logs in to the ssh host. Checks for standard prompts, and calls
\r
259 the function passed as promptcb to process them.
\r
260 Returns the login banner, or 'None' if login process aborted.
\r
263 banner = self.read_some()
\r
264 if self.debuglevel:
\r
265 print ">> 1st banner read is: %s" % banner
\r
266 while banner.find(logintext) == -1:
\r
267 response, abort = prompt_callback(banner)
\r
269 return self.close()
\r
270 self.write(response + '\n')
\r
271 banner = self.read_some()
\r
275 """Logs out the session."""
\r
278 def sendcmd(self, cmd, readtype=READ_SOME, expected=None, timeout=None):
\r
279 """Sends the command 'cmd' over the ssh connection, and returns the
\r
280 result. By default it uses read_some, which may block.
\r
282 if cmd[-1] != '\n':
\r
285 if readtype == READ_ALL:
\r
286 return self.read_all()
\r
287 elif readtype == READ_LAZY:
\r
288 return self.read_lazy()
\r
289 elif readtype == READ_UNTIL and expected is not None:
\r
290 return self.read_until(expected, timeout)
\r
292 return self.read_some()
\r
295 """Test routine for myssh.
\r
297 Usage: python myssh.py [-d] [-sshp path-to-ssh] [username@host | host] [port]
\r
299 Default host is localhost, default port is 22.
\r
303 if sys.argv[1:] and sys.argv[1] == '-d':
\r
306 testsshpath = SSH_PATH
\r
307 if sys.argv[1:] and sys.argv[1] == '-sshp':
\r
308 testsshpath = sys.argv[2]
\r
311 testusername = None
\r
312 testhost = 'localhost'
\r
315 testhost = sys.argv[1]
\r
316 if testhost.find('@') != -1:
\r
317 testusername, testhost = testhost.split('@')
\r
319 testport = sys.argv[2]
\r
321 testcon = Ssh(testusername, testhost, testport)
\r
322 testcon.set_debuglevel(debug)
\r
323 testcon.set_sshpath(testsshpath)
\r
327 while (cmd != 'exit') and testcon.isopen:
\r
328 cmd = raw_input("Enter command to send: ")
\r
329 print testcon.sendcmd(cmd)
\r
332 if __name__ == '__main__':
\r