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
148 if not os.path.isfile(self.sshpath):
\r
149 raise mysshError, \
\r
150 "Path to ssh or plink is not defined or invalid.\nsshpath='%s'" \
\r
153 raise mysshError, "Connection already open."
\r
155 if self.sshpath.lower().find('plink') != -1:
\r
157 if self.port and self.port != '':
\r
158 sshargs += PORT_STR + self.port + ' '
\r
159 sshargs += " -o StrictHostKeyChecking=no -o PasswordAuthentication=yes -o PubkeyAuthentication=no "
\r
160 if self.username and self.username !='':
\r
161 sshargs += self.username + '@'
\r
162 sshargs += self.host
\r
164 sshargs += ' ' + cmd
\r
165 if self.debuglevel:
\r
166 print ">> Running %s %s." % (self.sshpath, sshargs)
\r
167 # temporary workaround until I get pid's working under win32
\r
169 if os.name == 'posix':
\r
170 self.sshin, self.sshoutblocking, self.sshpid = \
\r
171 sshpopen2(self.sshpath + ' ' + sshargs)
\r
173 self.sshin, self.sshoutblocking = \
\r
174 sshpopen2(self.sshpath + ' ' + sshargs)
\r
175 self.sshout = nbpipe.nbpipe(self.sshoutblocking)
\r
177 if self.debuglevel:
\r
178 print ">> ssh pid is %s." % self.sshpid
\r
180 def close(self, addnewline=1):
\r
181 """Close the ssh connection by closing the input and output pipes.
\r
182 Returns the closing messages.
\r
184 On Posix systems, by default it adds a newline before sending the
\r
185 disconnect escape sequence. Turn this off by setting addnewline=0.
\r
187 if os.name == 'posix':
\r
191 self.write(CLOSE_STR)
\r
192 except (OSError, IOError, mysshError):
\r
194 output = self.read_lazy()
\r
197 self.sshoutblocking.close()
\r
200 if os.name == 'posix':
\r
202 os.kill(self.sshpid, signal.SIGHUP)
\r
203 os.waitpid(self.sshpid, 0)
\r
207 if self.debuglevel:
\r
208 print ">> Connection closed."
\r
211 def write(self, text):
\r
212 """Send text to the ssh process."""
\r
213 # May block?? Probably not in practice, as ssh has a large input buffer.
\r
214 if self.debuglevel:
\r
215 print ">> Sending %s" % text
\r
218 numtaken = os.write(self.sshin.fileno(),text)
\r
219 if self.debuglevel:
\r
220 print ">> %s characters taken" % numtaken
\r
221 text = text[numtaken:]
\r
223 raise mysshError, "Attempted to write to closed connection."
\r
225 # There is a question about what to do with connections closed by the other
\r
226 # end. Should write and read check for this, and force proper close?
\r
227 def read_lazy(self):
\r
228 """Lazy read from sshout. Waits a little, but does not block."""
\r
229 return self.sshout.read_lazy()
\r
231 def read_some(self):
\r
232 """Always read at least one block, unless the connection is closed.
\r
235 return self.sshout.read_some()
\r
237 return self.sshout.read_lazy()
\r
239 def read_until(self, match, timeout=None):
\r
240 """Read until a given string is encountered or until timeout.
\r
242 When no match is found, return whatever is available instead,
\r
243 possibly the empty string. Raise EOFError if the connection
\r
244 is closed and no cooked data is available.
\r
248 return self.sshout.read_until(match, timeout)
\r
250 return self.sshout.read_lazy()
\r
252 def read_all(self):
\r
253 """Reads until end of file hit. May block."""
\r
255 return self.sshout.read_all()
\r
257 return self.sshout.read_lazy()
\r
259 # High level funcitons
\r
260 def login(self, logintext='Last login:', prompt_callback=_prompt):
\r
261 """Logs in to the ssh host. Checks for standard prompts, and calls
\r
262 the function passed as promptcb to process them.
\r
263 Returns the login banner, or 'None' if login process aborted.
\r
266 banner = self.read_some()
\r
267 if self.debuglevel:
\r
268 print ">> 1st banner read is: %s" % banner
\r
269 while banner.find(logintext) == -1:
\r
270 response, abort = prompt_callback(banner)
\r
272 return self.close()
\r
273 self.write(response + '\n')
\r
274 banner = self.read_some()
\r
278 """Logs out the session."""
\r
281 def sendcmd(self, cmd, readtype=READ_SOME, expected=None, timeout=None):
\r
282 """Sends the command 'cmd' over the ssh connection, and returns the
\r
283 result. By default it uses read_some, which may block.
\r
285 if cmd[-1] != '\n':
\r
288 if readtype == READ_ALL:
\r
289 return self.read_all()
\r
290 elif readtype == READ_LAZY:
\r
291 return self.read_lazy()
\r
292 elif readtype == READ_UNTIL and expected is not None:
\r
293 return self.read_until(expected, timeout)
\r
295 return self.read_some()
\r
298 """Test routine for myssh.
\r
300 Usage: python myssh.py [-d] [-sshp path-to-ssh] [username@host | host] [port]
\r
302 Default host is localhost, default port is 22.
\r
306 if sys.argv[1:] and sys.argv[1] == '-d':
\r
309 testsshpath = SSH_PATH
\r
310 if sys.argv[1:] and sys.argv[1] == '-sshp':
\r
311 testsshpath = sys.argv[2]
\r
314 testusername = None
\r
315 testhost = 'localhost'
\r
318 testhost = sys.argv[1]
\r
319 if testhost.find('@') != -1:
\r
320 testusername, testhost = testhost.split('@')
\r
322 testport = sys.argv[2]
\r
324 testcon = Ssh(testusername, testhost, testport)
\r
325 testcon.set_debuglevel(debug)
\r
326 testcon.set_sshpath(testsshpath)
\r
330 while (cmd != 'exit') and testcon.isopen:
\r
331 cmd = raw_input("Enter command to send: ")
\r
332 print testcon.sendcmd(cmd)
\r
335 if __name__ == '__main__':
\r