Skip to content

Commit 16ca111

Browse files
committed
ssh: better OpenSSH 7.5+ permission denied handling
The user@host prefix in new-style OpenSSH messages unfortunately takes the host part from ~/.ssh/config and friends. There is no way to know which hostname will appear in this string without parsing the OpenSSH config, nor which username will appear. Instead just regex it. Add SSH stub modes to print the new/old errors and add some simple tests. This extends the work done in b9112a9
1 parent b527ff0 commit 16ca111

File tree

3 files changed

+43
-11
lines changed

3 files changed

+43
-11
lines changed

mitogen/ssh.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"""
3232

3333
import logging
34+
import re
3435
import time
3536

3637
try:
@@ -46,10 +47,16 @@
4647

4748
# sshpass uses 'assword' because it doesn't lowercase the input.
4849
PASSWORD_PROMPT = b('password')
49-
PERMDENIED_PROMPT = b('permission denied')
5050
HOSTKEY_REQ_PROMPT = b('are you sure you want to continue connecting (yes/no)?')
5151
HOSTKEY_FAIL = b('host key verification failed.')
5252

53+
# [user@host: ] permission denied
54+
PERMDENIED_RE = re.compile(
55+
('(?:[^@]+@[^:]+: )?' # Absent in OpenSSH <7.5
56+
'Permission denied').encode(),
57+
re.I
58+
)
59+
5360

5461
DEBUG_PREFIXES = (b('debug1:'), b('debug2:'), b('debug3:'))
5562

@@ -289,11 +296,7 @@ def _connect_bootstrap(self, extra_fd):
289296
self._host_key_prompt()
290297
elif HOSTKEY_FAIL in buf.lower():
291298
raise HostKeyError(self.hostkey_failed_msg)
292-
elif buf.lower().startswith((
293-
PERMDENIED_PROMPT,
294-
b("%s@%s: " % (self.username, self.hostname))
295-
+ PERMDENIED_PROMPT,
296-
)):
299+
elif PERMDENIED_RE.match(buf):
297300
# issue #271: work around conflict with user shell reporting
298301
# 'permission denied' e.g. during chdir($HOME) by only matching
299302
# it at the start of the line.

tests/data/stubs/ssh.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
HOST_KEY_STRICT_MSG = """Host key verification failed.\n"""
1717

18+
PERMDENIED_CLASSIC_MSG = 'Permission denied (publickey,password)\n'
19+
PERMDENIED_75_MSG = 'chicken@nandos.com: permission denied (publickey,password)\n'
20+
21+
1822

1923
def tty(msg):
2024
fp = open('/dev/tty', 'wb', 0)
@@ -37,13 +41,23 @@ def confirm(msg):
3741
fp.close()
3842

3943

40-
if os.getenv('FAKESSH_MODE') == 'ask':
44+
mode = os.getenv('FAKESSH_MODE')
45+
46+
if mode == 'ask':
4147
assert 'y\n' == confirm(HOST_KEY_ASK_MSG)
4248

43-
if os.getenv('FAKESSH_MODE') == 'strict':
49+
elif mode == 'strict':
4450
stderr(HOST_KEY_STRICT_MSG)
4551
sys.exit(255)
4652

53+
elif mode == 'permdenied_classic':
54+
stderr(PERMDENIED_CLASSIC_MSG)
55+
sys.exit(255)
56+
57+
elif mode == 'permdenied_75':
58+
stderr(PERMDENIED_75_MSG)
59+
sys.exit(255)
60+
4761

4862
#
4963
# Set an env var if stderr was a TTY to make ssh_test tests easier to write.

tests/ssh_test.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,10 @@ def test_verbose_enabled(self):
124124
self.assertEquals(name, context.name)
125125

126126

127-
class RequirePtyTest(testlib.DockerMixin, testlib.TestCase):
128-
stream_class = mitogen.ssh.Stream
129-
127+
class FakeSshMixin(testlib.RouterMixin):
128+
"""
129+
Mix-in that provides :meth:`fake_ssh` executing the stub 'ssh.py'.
130+
"""
130131
def fake_ssh(self, FAKESSH_MODE=None, **kwargs):
131132
os.environ['FAKESSH_MODE'] = str(FAKESSH_MODE)
132133
try:
@@ -139,6 +140,20 @@ def fake_ssh(self, FAKESSH_MODE=None, **kwargs):
139140
finally:
140141
del os.environ['FAKESSH_MODE']
141142

143+
144+
class PermissionDeniedTest(FakeSshMixin, testlib.TestCase):
145+
def test_classic_prompt(self):
146+
self.assertRaises(mitogen.ssh.PasswordError,
147+
lambda: self.fake_ssh(FAKESSH_MODE='permdenied_classic'))
148+
149+
def test_openssh_75_prompt(self):
150+
self.assertRaises(mitogen.ssh.PasswordError,
151+
lambda: self.fake_ssh(FAKESSH_MODE='permdenied_75'))
152+
153+
154+
class RequirePtyTest(FakeSshMixin, testlib.TestCase):
155+
stream_class = mitogen.ssh.Stream
156+
142157
def test_check_host_keys_accept(self):
143158
# required=true, host_key_checking=accept
144159
context = self.fake_ssh(FAKESSH_MODE='ask', check_host_keys='accept')

0 commit comments

Comments
 (0)