xref: /openbsd-src/gnu/llvm/lldb/third_party/Python/module/pexpect-4.6/pexpect/pxssh.py (revision 061da546b983eb767bad15e67af1174fb0bcf31c)
1*061da546Spatrick'''This class extends pexpect.spawn to specialize setting up SSH connections.
2*061da546SpatrickThis adds methods for login, logout, and expecting the shell prompt.
3*061da546Spatrick
4*061da546SpatrickPEXPECT LICENSE
5*061da546Spatrick
6*061da546Spatrick    This license is approved by the OSI and FSF as GPL-compatible.
7*061da546Spatrick        http://opensource.org/licenses/isc-license.txt
8*061da546Spatrick
9*061da546Spatrick    Copyright (c) 2012, Noah Spurrier <noah@noah.org>
10*061da546Spatrick    PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
11*061da546Spatrick    PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
12*061da546Spatrick    COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
13*061da546Spatrick    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
14*061da546Spatrick    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
15*061da546Spatrick    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
16*061da546Spatrick    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
17*061da546Spatrick    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
18*061da546Spatrick    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
19*061da546Spatrick    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20*061da546Spatrick
21*061da546Spatrick'''
22*061da546Spatrick
23*061da546Spatrickfrom pexpect import ExceptionPexpect, TIMEOUT, EOF, spawn
24*061da546Spatrickimport time
25*061da546Spatrickimport os
26*061da546Spatrickimport sys
27*061da546Spatrickimport re
28*061da546Spatrick
29*061da546Spatrick__all__ = ['ExceptionPxssh', 'pxssh']
30*061da546Spatrick
31*061da546Spatrick# Exception classes used by this module.
32*061da546Spatrickclass ExceptionPxssh(ExceptionPexpect):
33*061da546Spatrick    '''Raised for pxssh exceptions.
34*061da546Spatrick    '''
35*061da546Spatrick
36*061da546Spatrickif sys.version_info > (3, 0):
37*061da546Spatrick    from shlex import quote
38*061da546Spatrickelse:
39*061da546Spatrick    _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
40*061da546Spatrick
41*061da546Spatrick    def quote(s):
42*061da546Spatrick        """Return a shell-escaped version of the string *s*."""
43*061da546Spatrick        if not s:
44*061da546Spatrick            return "''"
45*061da546Spatrick        if _find_unsafe(s) is None:
46*061da546Spatrick            return s
47*061da546Spatrick
48*061da546Spatrick        # use single quotes, and put single quotes into double quotes
49*061da546Spatrick        # the string $'b is then quoted as '$'"'"'b'
50*061da546Spatrick        return "'" + s.replace("'", "'\"'\"'") + "'"
51*061da546Spatrick
52*061da546Spatrickclass pxssh (spawn):
53*061da546Spatrick    '''This class extends pexpect.spawn to specialize setting up SSH
54*061da546Spatrick    connections. This adds methods for login, logout, and expecting the shell
55*061da546Spatrick    prompt. It does various tricky things to handle many situations in the SSH
56*061da546Spatrick    login process. For example, if the session is your first login, then pxssh
57*061da546Spatrick    automatically accepts the remote certificate; or if you have public key
58*061da546Spatrick    authentication setup then pxssh won't wait for the password prompt.
59*061da546Spatrick
60*061da546Spatrick    pxssh uses the shell prompt to synchronize output from the remote host. In
61*061da546Spatrick    order to make this more robust it sets the shell prompt to something more
62*061da546Spatrick    unique than just $ or #. This should work on most Borne/Bash or Csh style
63*061da546Spatrick    shells.
64*061da546Spatrick
65*061da546Spatrick    Example that runs a few commands on a remote server and prints the result::
66*061da546Spatrick
67*061da546Spatrick        from pexpect import pxssh
68*061da546Spatrick        import getpass
69*061da546Spatrick        try:
70*061da546Spatrick            s = pxssh.pxssh()
71*061da546Spatrick            hostname = raw_input('hostname: ')
72*061da546Spatrick            username = raw_input('username: ')
73*061da546Spatrick            password = getpass.getpass('password: ')
74*061da546Spatrick            s.login(hostname, username, password)
75*061da546Spatrick            s.sendline('uptime')   # run a command
76*061da546Spatrick            s.prompt()             # match the prompt
77*061da546Spatrick            print(s.before)        # print everything before the prompt.
78*061da546Spatrick            s.sendline('ls -l')
79*061da546Spatrick            s.prompt()
80*061da546Spatrick            print(s.before)
81*061da546Spatrick            s.sendline('df')
82*061da546Spatrick            s.prompt()
83*061da546Spatrick            print(s.before)
84*061da546Spatrick            s.logout()
85*061da546Spatrick        except pxssh.ExceptionPxssh as e:
86*061da546Spatrick            print("pxssh failed on login.")
87*061da546Spatrick            print(e)
88*061da546Spatrick
89*061da546Spatrick    Example showing how to specify SSH options::
90*061da546Spatrick
91*061da546Spatrick        from pexpect import pxssh
92*061da546Spatrick        s = pxssh.pxssh(options={
93*061da546Spatrick                            "StrictHostKeyChecking": "no",
94*061da546Spatrick                            "UserKnownHostsFile": "/dev/null"})
95*061da546Spatrick        ...
96*061da546Spatrick
97*061da546Spatrick    Note that if you have ssh-agent running while doing development with pxssh
98*061da546Spatrick    then this can lead to a lot of confusion. Many X display managers (xdm,
99*061da546Spatrick    gdm, kdm, etc.) will automatically start a GUI agent. You may see a GUI
100*061da546Spatrick    dialog box popup asking for a password during development. You should turn
101*061da546Spatrick    off any key agents during testing. The 'force_password' attribute will turn
102*061da546Spatrick    off public key authentication. This will only work if the remote SSH server
103*061da546Spatrick    is configured to allow password logins. Example of using 'force_password'
104*061da546Spatrick    attribute::
105*061da546Spatrick
106*061da546Spatrick            s = pxssh.pxssh()
107*061da546Spatrick            s.force_password = True
108*061da546Spatrick            hostname = raw_input('hostname: ')
109*061da546Spatrick            username = raw_input('username: ')
110*061da546Spatrick            password = getpass.getpass('password: ')
111*061da546Spatrick            s.login (hostname, username, password)
112*061da546Spatrick
113*061da546Spatrick    `debug_command_string` is only for the test suite to confirm that the string
114*061da546Spatrick    generated for SSH is correct, using this will not allow you to do
115*061da546Spatrick    anything other than get a string back from `pxssh.pxssh.login()`.
116*061da546Spatrick    '''
117*061da546Spatrick
118*061da546Spatrick    def __init__ (self, timeout=30, maxread=2000, searchwindowsize=None,
119*061da546Spatrick                    logfile=None, cwd=None, env=None, ignore_sighup=True, echo=True,
120*061da546Spatrick                    options={}, encoding=None, codec_errors='strict',
121*061da546Spatrick                    debug_command_string=False):
122*061da546Spatrick
123*061da546Spatrick        spawn.__init__(self, None, timeout=timeout, maxread=maxread,
124*061da546Spatrick                       searchwindowsize=searchwindowsize, logfile=logfile,
125*061da546Spatrick                       cwd=cwd, env=env, ignore_sighup=ignore_sighup, echo=echo,
126*061da546Spatrick                       encoding=encoding, codec_errors=codec_errors)
127*061da546Spatrick
128*061da546Spatrick        self.name = '<pxssh>'
129*061da546Spatrick
130*061da546Spatrick        #SUBTLE HACK ALERT! Note that the command that SETS the prompt uses a
131*061da546Spatrick        #slightly different string than the regular expression to match it. This
132*061da546Spatrick        #is because when you set the prompt the command will echo back, but we
133*061da546Spatrick        #don't want to match the echoed command. So if we make the set command
134*061da546Spatrick        #slightly different than the regex we eliminate the problem. To make the
135*061da546Spatrick        #set command different we add a backslash in front of $. The $ doesn't
136*061da546Spatrick        #need to be escaped, but it doesn't hurt and serves to make the set
137*061da546Spatrick        #prompt command different than the regex.
138*061da546Spatrick
139*061da546Spatrick        # used to match the command-line prompt
140*061da546Spatrick        self.UNIQUE_PROMPT = r"\[PEXPECT\][\$\#] "
141*061da546Spatrick        self.PROMPT = self.UNIQUE_PROMPT
142*061da546Spatrick
143*061da546Spatrick        # used to set shell command-line prompt to UNIQUE_PROMPT.
144*061da546Spatrick        self.PROMPT_SET_SH = r"PS1='[PEXPECT]\$ '"
145*061da546Spatrick        self.PROMPT_SET_CSH = r"set prompt='[PEXPECT]\$ '"
146*061da546Spatrick        self.SSH_OPTS = ("-o'RSAAuthentication=no'"
147*061da546Spatrick                + " -o 'PubkeyAuthentication=no'")
148*061da546Spatrick# Disabling host key checking, makes you vulnerable to MITM attacks.
149*061da546Spatrick#                + " -o 'StrictHostKeyChecking=no'"
150*061da546Spatrick#                + " -o 'UserKnownHostsFile /dev/null' ")
151*061da546Spatrick        # Disabling X11 forwarding gets rid of the annoying SSH_ASKPASS from
152*061da546Spatrick        # displaying a GUI password dialog. I have not figured out how to
153*061da546Spatrick        # disable only SSH_ASKPASS without also disabling X11 forwarding.
154*061da546Spatrick        # Unsetting SSH_ASKPASS on the remote side doesn't disable it! Annoying!
155*061da546Spatrick        #self.SSH_OPTS = "-x -o'RSAAuthentication=no' -o 'PubkeyAuthentication=no'"
156*061da546Spatrick        self.force_password = False
157*061da546Spatrick
158*061da546Spatrick        self.debug_command_string = debug_command_string
159*061da546Spatrick
160*061da546Spatrick        # User defined SSH options, eg,
161*061da546Spatrick        # ssh.otions = dict(StrictHostKeyChecking="no",UserKnownHostsFile="/dev/null")
162*061da546Spatrick        self.options = options
163*061da546Spatrick
164*061da546Spatrick    def levenshtein_distance(self, a, b):
165*061da546Spatrick        '''This calculates the Levenshtein distance between a and b.
166*061da546Spatrick        '''
167*061da546Spatrick
168*061da546Spatrick        n, m = len(a), len(b)
169*061da546Spatrick        if n > m:
170*061da546Spatrick            a,b = b,a
171*061da546Spatrick            n,m = m,n
172*061da546Spatrick        current = range(n+1)
173*061da546Spatrick        for i in range(1,m+1):
174*061da546Spatrick            previous, current = current, [i]+[0]*n
175*061da546Spatrick            for j in range(1,n+1):
176*061da546Spatrick                add, delete = previous[j]+1, current[j-1]+1
177*061da546Spatrick                change = previous[j-1]
178*061da546Spatrick                if a[j-1] != b[i-1]:
179*061da546Spatrick                    change = change + 1
180*061da546Spatrick                current[j] = min(add, delete, change)
181*061da546Spatrick        return current[n]
182*061da546Spatrick
183*061da546Spatrick    def try_read_prompt(self, timeout_multiplier):
184*061da546Spatrick        '''This facilitates using communication timeouts to perform
185*061da546Spatrick        synchronization as quickly as possible, while supporting high latency
186*061da546Spatrick        connections with a tunable worst case performance. Fast connections
187*061da546Spatrick        should be read almost immediately. Worst case performance for this
188*061da546Spatrick        method is timeout_multiplier * 3 seconds.
189*061da546Spatrick        '''
190*061da546Spatrick
191*061da546Spatrick        # maximum time allowed to read the first response
192*061da546Spatrick        first_char_timeout = timeout_multiplier * 0.5
193*061da546Spatrick
194*061da546Spatrick        # maximum time allowed between subsequent characters
195*061da546Spatrick        inter_char_timeout = timeout_multiplier * 0.1
196*061da546Spatrick
197*061da546Spatrick        # maximum time for reading the entire prompt
198*061da546Spatrick        total_timeout = timeout_multiplier * 3.0
199*061da546Spatrick
200*061da546Spatrick        prompt = self.string_type()
201*061da546Spatrick        begin = time.time()
202*061da546Spatrick        expired = 0.0
203*061da546Spatrick        timeout = first_char_timeout
204*061da546Spatrick
205*061da546Spatrick        while expired < total_timeout:
206*061da546Spatrick            try:
207*061da546Spatrick                prompt += self.read_nonblocking(size=1, timeout=timeout)
208*061da546Spatrick                expired = time.time() - begin # updated total time expired
209*061da546Spatrick                timeout = inter_char_timeout
210*061da546Spatrick            except TIMEOUT:
211*061da546Spatrick                break
212*061da546Spatrick
213*061da546Spatrick        return prompt
214*061da546Spatrick
215*061da546Spatrick    def sync_original_prompt (self, sync_multiplier=1.0):
216*061da546Spatrick        '''This attempts to find the prompt. Basically, press enter and record
217*061da546Spatrick        the response; press enter again and record the response; if the two
218*061da546Spatrick        responses are similar then assume we are at the original prompt.
219*061da546Spatrick        This can be a slow function. Worst case with the default sync_multiplier
220*061da546Spatrick        can take 12 seconds. Low latency connections are more likely to fail
221*061da546Spatrick        with a low sync_multiplier. Best case sync time gets worse with a
222*061da546Spatrick        high sync multiplier (500 ms with default). '''
223*061da546Spatrick
224*061da546Spatrick        # All of these timing pace values are magic.
225*061da546Spatrick        # I came up with these based on what seemed reliable for
226*061da546Spatrick        # connecting to a heavily loaded machine I have.
227*061da546Spatrick        self.sendline()
228*061da546Spatrick        time.sleep(0.1)
229*061da546Spatrick
230*061da546Spatrick        try:
231*061da546Spatrick            # Clear the buffer before getting the prompt.
232*061da546Spatrick            self.try_read_prompt(sync_multiplier)
233*061da546Spatrick        except TIMEOUT:
234*061da546Spatrick            pass
235*061da546Spatrick
236*061da546Spatrick        self.sendline()
237*061da546Spatrick        x = self.try_read_prompt(sync_multiplier)
238*061da546Spatrick
239*061da546Spatrick        self.sendline()
240*061da546Spatrick        a = self.try_read_prompt(sync_multiplier)
241*061da546Spatrick
242*061da546Spatrick        self.sendline()
243*061da546Spatrick        b = self.try_read_prompt(sync_multiplier)
244*061da546Spatrick
245*061da546Spatrick        ld = self.levenshtein_distance(a,b)
246*061da546Spatrick        len_a = len(a)
247*061da546Spatrick        if len_a == 0:
248*061da546Spatrick            return False
249*061da546Spatrick        if float(ld)/len_a < 0.4:
250*061da546Spatrick            return True
251*061da546Spatrick        return False
252*061da546Spatrick
253*061da546Spatrick    ### TODO: This is getting messy and I'm pretty sure this isn't perfect.
254*061da546Spatrick    ### TODO: I need to draw a flow chart for this.
255*061da546Spatrick    ### TODO: Unit tests for SSH tunnels, remote SSH command exec, disabling original prompt sync
256*061da546Spatrick    def login (self, server, username, password='', terminal_type='ansi',
257*061da546Spatrick                original_prompt=r"[#$]", login_timeout=10, port=None,
258*061da546Spatrick                auto_prompt_reset=True, ssh_key=None, quiet=True,
259*061da546Spatrick                sync_multiplier=1, check_local_ip=True,
260*061da546Spatrick                password_regex=r'(?i)(?:password:)|(?:passphrase for key)',
261*061da546Spatrick                ssh_tunnels={}, spawn_local_ssh=True,
262*061da546Spatrick                sync_original_prompt=True, ssh_config=None):
263*061da546Spatrick        '''This logs the user into the given server.
264*061da546Spatrick
265*061da546Spatrick        It uses
266*061da546Spatrick        'original_prompt' to try to find the prompt right after login. When it
267*061da546Spatrick        finds the prompt it immediately tries to reset the prompt to something
268*061da546Spatrick        more easily matched. The default 'original_prompt' is very optimistic
269*061da546Spatrick        and is easily fooled. It's more reliable to try to match the original
270*061da546Spatrick        prompt as exactly as possible to prevent false matches by server
271*061da546Spatrick        strings such as the "Message Of The Day". On many systems you can
272*061da546Spatrick        disable the MOTD on the remote server by creating a zero-length file
273*061da546Spatrick        called :file:`~/.hushlogin` on the remote server. If a prompt cannot be found
274*061da546Spatrick        then this will not necessarily cause the login to fail. In the case of
275*061da546Spatrick        a timeout when looking for the prompt we assume that the original
276*061da546Spatrick        prompt was so weird that we could not match it, so we use a few tricks
277*061da546Spatrick        to guess when we have reached the prompt. Then we hope for the best and
278*061da546Spatrick        blindly try to reset the prompt to something more unique. If that fails
279*061da546Spatrick        then login() raises an :class:`ExceptionPxssh` exception.
280*061da546Spatrick
281*061da546Spatrick        In some situations it is not possible or desirable to reset the
282*061da546Spatrick        original prompt. In this case, pass ``auto_prompt_reset=False`` to
283*061da546Spatrick        inhibit setting the prompt to the UNIQUE_PROMPT. Remember that pxssh
284*061da546Spatrick        uses a unique prompt in the :meth:`prompt` method. If the original prompt is
285*061da546Spatrick        not reset then this will disable the :meth:`prompt` method unless you
286*061da546Spatrick        manually set the :attr:`PROMPT` attribute.
287*061da546Spatrick
288*061da546Spatrick        Set ``password_regex`` if there is a MOTD message with `password` in it.
289*061da546Spatrick        Changing this is like playing in traffic, don't (p)expect it to match straight
290*061da546Spatrick        away.
291*061da546Spatrick
292*061da546Spatrick        If you require to connect to another SSH server from the your original SSH
293*061da546Spatrick        connection set ``spawn_local_ssh`` to `False` and this will use your current
294*061da546Spatrick        session to do so. Setting this option to `False` and not having an active session
295*061da546Spatrick        will trigger an error.
296*061da546Spatrick
297*061da546Spatrick        Set ``ssh_key`` to a file path to an SSH private key to use that SSH key
298*061da546Spatrick        for the session authentication.
299*061da546Spatrick        Set ``ssh_key`` to `True` to force passing the current SSH authentication socket
300*061da546Spatrick        to the desired ``hostname``.
301*061da546Spatrick
302*061da546Spatrick        Set ``ssh_config`` to a file path string of an SSH client config file to pass that
303*061da546Spatrick        file to the client to handle itself. You may set any options you wish in here, however
304*061da546Spatrick        doing so will require you to post extra information that you may not want to if you
305*061da546Spatrick        run into issues.
306*061da546Spatrick        '''
307*061da546Spatrick
308*061da546Spatrick        session_regex_array = ["(?i)are you sure you want to continue connecting", original_prompt, password_regex, "(?i)permission denied", "(?i)terminal type", TIMEOUT]
309*061da546Spatrick        session_init_regex_array = []
310*061da546Spatrick        session_init_regex_array.extend(session_regex_array)
311*061da546Spatrick        session_init_regex_array.extend(["(?i)connection closed by remote host", EOF])
312*061da546Spatrick
313*061da546Spatrick        ssh_options = ''.join([" -o '%s=%s'" % (o, v) for (o, v) in self.options.items()])
314*061da546Spatrick        if quiet:
315*061da546Spatrick            ssh_options = ssh_options + ' -q'
316*061da546Spatrick        if not check_local_ip:
317*061da546Spatrick            ssh_options = ssh_options + " -o'NoHostAuthenticationForLocalhost=yes'"
318*061da546Spatrick        if self.force_password:
319*061da546Spatrick            ssh_options = ssh_options + ' ' + self.SSH_OPTS
320*061da546Spatrick        if ssh_config is not None:
321*061da546Spatrick            if spawn_local_ssh and not os.path.isfile(ssh_config):
322*061da546Spatrick                raise ExceptionPxssh('SSH config does not exist or is not a file.')
323*061da546Spatrick            ssh_options = ssh_options + '-F ' + ssh_config
324*061da546Spatrick        if port is not None:
325*061da546Spatrick            ssh_options = ssh_options + ' -p %s'%(str(port))
326*061da546Spatrick        if ssh_key is not None:
327*061da546Spatrick            # Allow forwarding our SSH key to the current session
328*061da546Spatrick            if ssh_key==True:
329*061da546Spatrick                ssh_options = ssh_options + ' -A'
330*061da546Spatrick            else:
331*061da546Spatrick                if spawn_local_ssh and not os.path.isfile(ssh_key):
332*061da546Spatrick                    raise ExceptionPxssh('private ssh key does not exist or is not a file.')
333*061da546Spatrick                ssh_options = ssh_options + ' -i %s' % (ssh_key)
334*061da546Spatrick
335*061da546Spatrick        # SSH tunnels, make sure you know what you're putting into the lists
336*061da546Spatrick        # under each heading. Do not expect these to open 100% of the time,
337*061da546Spatrick        # The port you're requesting might be bound.
338*061da546Spatrick        #
339*061da546Spatrick        # The structure should be like this:
340*061da546Spatrick        # { 'local': ['2424:localhost:22'],  # Local SSH tunnels
341*061da546Spatrick        # 'remote': ['2525:localhost:22'],   # Remote SSH tunnels
342*061da546Spatrick        # 'dynamic': [8888] } # Dynamic/SOCKS tunnels
343*061da546Spatrick        if ssh_tunnels!={} and isinstance({},type(ssh_tunnels)):
344*061da546Spatrick            tunnel_types = {
345*061da546Spatrick                'local':'L',
346*061da546Spatrick                'remote':'R',
347*061da546Spatrick                'dynamic':'D'
348*061da546Spatrick            }
349*061da546Spatrick            for tunnel_type in tunnel_types:
350*061da546Spatrick                cmd_type = tunnel_types[tunnel_type]
351*061da546Spatrick                if tunnel_type in ssh_tunnels:
352*061da546Spatrick                    tunnels = ssh_tunnels[tunnel_type]
353*061da546Spatrick                    for tunnel in tunnels:
354*061da546Spatrick                        if spawn_local_ssh==False:
355*061da546Spatrick                            tunnel = quote(str(tunnel))
356*061da546Spatrick                        ssh_options = ssh_options + ' -' + cmd_type + ' ' + str(tunnel)
357*061da546Spatrick        cmd = "ssh %s -l %s %s" % (ssh_options, username, server)
358*061da546Spatrick        if self.debug_command_string:
359*061da546Spatrick            return(cmd)
360*061da546Spatrick
361*061da546Spatrick        # Are we asking for a local ssh command or to spawn one in another session?
362*061da546Spatrick        if spawn_local_ssh:
363*061da546Spatrick            spawn._spawn(self, cmd)
364*061da546Spatrick        else:
365*061da546Spatrick            self.sendline(cmd)
366*061da546Spatrick
367*061da546Spatrick        # This does not distinguish between a remote server 'password' prompt
368*061da546Spatrick        # and a local ssh 'passphrase' prompt (for unlocking a private key).
369*061da546Spatrick        i = self.expect(session_init_regex_array, timeout=login_timeout)
370*061da546Spatrick
371*061da546Spatrick        # First phase
372*061da546Spatrick        if i==0:
373*061da546Spatrick            # New certificate -- always accept it.
374*061da546Spatrick            # This is what you get if SSH does not have the remote host's
375*061da546Spatrick            # public key stored in the 'known_hosts' cache.
376*061da546Spatrick            self.sendline("yes")
377*061da546Spatrick            i = self.expect(session_regex_array)
378*061da546Spatrick        if i==2: # password or passphrase
379*061da546Spatrick            self.sendline(password)
380*061da546Spatrick            i = self.expect(session_regex_array)
381*061da546Spatrick        if i==4:
382*061da546Spatrick            self.sendline(terminal_type)
383*061da546Spatrick            i = self.expect(session_regex_array)
384*061da546Spatrick        if i==7:
385*061da546Spatrick            self.close()
386*061da546Spatrick            raise ExceptionPxssh('Could not establish connection to host')
387*061da546Spatrick
388*061da546Spatrick        # Second phase
389*061da546Spatrick        if i==0:
390*061da546Spatrick            # This is weird. This should not happen twice in a row.
391*061da546Spatrick            self.close()
392*061da546Spatrick            raise ExceptionPxssh('Weird error. Got "are you sure" prompt twice.')
393*061da546Spatrick        elif i==1: # can occur if you have a public key pair set to authenticate.
394*061da546Spatrick            ### TODO: May NOT be OK if expect() got tricked and matched a false prompt.
395*061da546Spatrick            pass
396*061da546Spatrick        elif i==2: # password prompt again
397*061da546Spatrick            # For incorrect passwords, some ssh servers will
398*061da546Spatrick            # ask for the password again, others return 'denied' right away.
399*061da546Spatrick            # If we get the password prompt again then this means
400*061da546Spatrick            # we didn't get the password right the first time.
401*061da546Spatrick            self.close()
402*061da546Spatrick            raise ExceptionPxssh('password refused')
403*061da546Spatrick        elif i==3: # permission denied -- password was bad.
404*061da546Spatrick            self.close()
405*061da546Spatrick            raise ExceptionPxssh('permission denied')
406*061da546Spatrick        elif i==4: # terminal type again? WTF?
407*061da546Spatrick            self.close()
408*061da546Spatrick            raise ExceptionPxssh('Weird error. Got "terminal type" prompt twice.')
409*061da546Spatrick        elif i==5: # Timeout
410*061da546Spatrick            #This is tricky... I presume that we are at the command-line prompt.
411*061da546Spatrick            #It may be that the shell prompt was so weird that we couldn't match
412*061da546Spatrick            #it. Or it may be that we couldn't log in for some other reason. I
413*061da546Spatrick            #can't be sure, but it's safe to guess that we did login because if
414*061da546Spatrick            #I presume wrong and we are not logged in then this should be caught
415*061da546Spatrick            #later when I try to set the shell prompt.
416*061da546Spatrick            pass
417*061da546Spatrick        elif i==6: # Connection closed by remote host
418*061da546Spatrick            self.close()
419*061da546Spatrick            raise ExceptionPxssh('connection closed')
420*061da546Spatrick        else: # Unexpected
421*061da546Spatrick            self.close()
422*061da546Spatrick            raise ExceptionPxssh('unexpected login response')
423*061da546Spatrick        if sync_original_prompt:
424*061da546Spatrick            if not self.sync_original_prompt(sync_multiplier):
425*061da546Spatrick                self.close()
426*061da546Spatrick                raise ExceptionPxssh('could not synchronize with original prompt')
427*061da546Spatrick        # We appear to be in.
428*061da546Spatrick        # set shell prompt to something unique.
429*061da546Spatrick        if auto_prompt_reset:
430*061da546Spatrick            if not self.set_unique_prompt():
431*061da546Spatrick                self.close()
432*061da546Spatrick                raise ExceptionPxssh('could not set shell prompt '
433*061da546Spatrick                                     '(received: %r, expected: %r).' % (
434*061da546Spatrick                                         self.before, self.PROMPT,))
435*061da546Spatrick        return True
436*061da546Spatrick
437*061da546Spatrick    def logout (self):
438*061da546Spatrick        '''Sends exit to the remote shell.
439*061da546Spatrick
440*061da546Spatrick        If there are stopped jobs then this automatically sends exit twice.
441*061da546Spatrick        '''
442*061da546Spatrick        self.sendline("exit")
443*061da546Spatrick        index = self.expect([EOF, "(?i)there are stopped jobs"])
444*061da546Spatrick        if index==1:
445*061da546Spatrick            self.sendline("exit")
446*061da546Spatrick            self.expect(EOF)
447*061da546Spatrick        self.close()
448*061da546Spatrick
449*061da546Spatrick    def prompt(self, timeout=-1):
450*061da546Spatrick        '''Match the next shell prompt.
451*061da546Spatrick
452*061da546Spatrick        This is little more than a short-cut to the :meth:`~pexpect.spawn.expect`
453*061da546Spatrick        method. Note that if you called :meth:`login` with
454*061da546Spatrick        ``auto_prompt_reset=False``, then before calling :meth:`prompt` you must
455*061da546Spatrick        set the :attr:`PROMPT` attribute to a regex that it will use for
456*061da546Spatrick        matching the prompt.
457*061da546Spatrick
458*061da546Spatrick        Calling :meth:`prompt` will erase the contents of the :attr:`before`
459*061da546Spatrick        attribute even if no prompt is ever matched. If timeout is not given or
460*061da546Spatrick        it is set to -1 then self.timeout is used.
461*061da546Spatrick
462*061da546Spatrick        :return: True if the shell prompt was matched, False if the timeout was
463*061da546Spatrick                 reached.
464*061da546Spatrick        '''
465*061da546Spatrick
466*061da546Spatrick        if timeout == -1:
467*061da546Spatrick            timeout = self.timeout
468*061da546Spatrick        i = self.expect([self.PROMPT, TIMEOUT], timeout=timeout)
469*061da546Spatrick        if i==1:
470*061da546Spatrick            return False
471*061da546Spatrick        return True
472*061da546Spatrick
473*061da546Spatrick    def set_unique_prompt(self):
474*061da546Spatrick        '''This sets the remote prompt to something more unique than ``#`` or ``$``.
475*061da546Spatrick        This makes it easier for the :meth:`prompt` method to match the shell prompt
476*061da546Spatrick        unambiguously. This method is called automatically by the :meth:`login`
477*061da546Spatrick        method, but you may want to call it manually if you somehow reset the
478*061da546Spatrick        shell prompt. For example, if you 'su' to a different user then you
479*061da546Spatrick        will need to manually reset the prompt. This sends shell commands to
480*061da546Spatrick        the remote host to set the prompt, so this assumes the remote host is
481*061da546Spatrick        ready to receive commands.
482*061da546Spatrick
483*061da546Spatrick        Alternatively, you may use your own prompt pattern. In this case you
484*061da546Spatrick        should call :meth:`login` with ``auto_prompt_reset=False``; then set the
485*061da546Spatrick        :attr:`PROMPT` attribute to a regular expression. After that, the
486*061da546Spatrick        :meth:`prompt` method will try to match your prompt pattern.
487*061da546Spatrick        '''
488*061da546Spatrick
489*061da546Spatrick        self.sendline("unset PROMPT_COMMAND")
490*061da546Spatrick        self.sendline(self.PROMPT_SET_SH) # sh-style
491*061da546Spatrick        i = self.expect ([TIMEOUT, self.PROMPT], timeout=10)
492*061da546Spatrick        if i == 0: # csh-style
493*061da546Spatrick            self.sendline(self.PROMPT_SET_CSH)
494*061da546Spatrick            i = self.expect([TIMEOUT, self.PROMPT], timeout=10)
495*061da546Spatrick            if i == 0:
496*061da546Spatrick                return False
497*061da546Spatrick        return True
498*061da546Spatrick
499*061da546Spatrick# vi:ts=4:sw=4:expandtab:ft=python:
500