From 6e9f8e829efd4927433e290cb3ed1628f62d133b Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 28 Jan 2019 00:48:14 +0000 Subject: [PATCH] issue #429: teach sudo about every know i18n password string. --- misc/pogrep.py | 40 ++++++++++++++++++++++++ mitogen/sudo.py | 77 ++++++++++++++++++++++++++++++++++++++++++++-- tests/sudo_test.py | 41 ++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 misc/pogrep.py diff --git a/misc/pogrep.py b/misc/pogrep.py new file mode 100644 index 00000000..b837bcfd --- /dev/null +++ b/misc/pogrep.py @@ -0,0 +1,40 @@ + +# issue #429: tool for extracting keys out of message catalogs and turning them +# into the big gob of base64 as used in mitogen/sudo.py +# +# Usage: +# - apt-get source libpam0g +# - cd */po/ +# - python ~/pogrep.py "Password: " + +import sys +import shlex +import glob + + +last_word = None + +for path in glob.glob('*.po'): + for line in open(path): + bits = shlex.split(line, comments=True) + if not bits: + continue + + word = bits[0] + if len(bits) < 2 or not word: + continue + + rest = bits[1] + if not rest: + continue + + if last_word == 'msgid' and word == 'msgstr': + if last_rest == sys.argv[1]: + thing = rest.rstrip(': ').decode('utf-8').lower().encode('utf-8').encode('base64').rstrip() + print ' %-60s # %s' % (repr(thing)+',', path) + + last_word = word + last_rest = rest + +#ag -A 1 'msgid "Password: "'|less | grep msgstr | grep -v '""'|cut -d'"' -f2|cut -d'"' -f1| tr -d : + diff --git a/mitogen/sudo.py b/mitogen/sudo.py index 221b8391..05483f7e 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -26,8 +26,10 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +import base64 import logging import optparse +import re import mitogen.core import mitogen.parent @@ -35,6 +37,73 @@ from mitogen.core import b LOG = logging.getLogger(__name__) + +# These are base64-encoded UTF-8 as our existing minifier/module server +# struggles with Unicode Python source in some (forgotten) circumstances. +PASSWORD_PROMPTS = [ + 'cGFzc3dvcmQ=', # english + 'bG96aW5rYQ==', # sr@latin.po + '44OR44K544Ov44O844OJ', # ja.po + '4Kaq4Ka+4Ka44KaT4Kef4Ka+4Kaw4KeN4Kah', # bn.po + '2YPZhNmF2Kkg2KfZhNiz2LE=', # ar.po + 'cGFzYWhpdHph', # eu.po + '0L/QsNGA0L7Qu9GM', # uk.po + 'cGFyb29s', # et.po + 'c2FsYXNhbmE=', # fi.po + '4Kiq4Ki+4Ki44Ki14Kiw4Kih', # pa.po + 'Y29udHJhc2lnbm8=', # ia.po + 'Zm9jYWwgZmFpcmU=', # ga.po + '16HXodee15Q=', # he.po + '4Kqq4Kq+4Kq44Kq14Kqw4KuN4Kqh', # gu.po + '0L/QsNGA0L7Qu9Cw', # bg.po + '4Kyq4K2N4Kyw4Kys4K2H4Ky2IOCsuOCsmeCtjeCsleCth+CspA==', # or.po + '4K6V4K6f4K614K+B4K6a4K+N4K6a4K+K4K6y4K+N', # ta.po + 'cGFzc3dvcnQ=', # de.po + '7JWU7Zi4', # ko.po + '0LvQvtC30LjQvdC60LA=', # sr.po + 'beG6rXQga2jhuql1', # vi.po + 'c2VuaGE=', # pt_BR.po + 'cGFzc3dvcmQ=', # it.po + 'aGVzbG8=', # cs.po + '5a+G56K877ya', # zh_TW.po + 'aGVzbG8=', # sk.po + '4LC44LCC4LCV4LGH4LCk4LCq4LCm4LCu4LGB', # te.po + '0L/QsNGA0L7Qu9GM', # kk.po + 'aGFzxYJv', # pl.po + 'Y29udHJhc2VueWE=', # ca.po + 'Y29udHJhc2XDsWE=', # es.po + '4LSF4LSf4LSv4LS+4LSz4LS14LS+4LSV4LWN4LSV4LWN', # ml.po + 'c2VuaGE=', # pt.po + '5a+G56CB77ya', # zh_CN.po + '4KSX4KWB4KSq4KWN4KSk4KS24KSs4KWN4KSm', # mr.po + 'bMO2c2Vub3Jk', # sv.po + '4YOe4YOQ4YOg4YOd4YOa4YOY', # ka.po + '4KS24KSs4KWN4KSm4KSV4KWC4KSf', # hi.po + 'YWRnYW5nc2tvZGU=', # da.po + '4La74LeE4LeD4LeK4La04Lav4La6', # si.po + 'cGFzc29yZA==', # nb.po + 'd2FjaHR3b29yZA==', # nl.po + '4Kaq4Ka+4Ka44KaT4Kef4Ka+4Kaw4KeN4Kah', # bn_IN.po + 'cGFyb2xh', # tr.po + '4LKX4LOB4LKq4LON4LKk4LKq4LKm', # kn.po + 'c2FuZGk=', # id.po + '0L/QsNGA0L7Qu9GM', # ru.po + 'amVsc3rDsw==', # hu.po + 'bW90IGRlIHBhc3Nl', # fr.po + 'aXBoYXNpd2VkaQ==', # zu.po + '4Z6W4Z624Z6A4Z+S4Z6Z4Z6f4Z6Y4Z+S4Z6E4Z624Z6P4Z+LwqDhn5Y=', # km.po + '4KaX4KeB4Kaq4KeN4Kak4Ka24Kas4KeN4Kam', # as.po +] + + +PASSWORD_PROMPT_RE = re.compile( + u'|'.join( + base64.b64decode(s).decode('utf-8') + for s in PASSWORD_PROMPTS + ) +) + + PASSWORD_PROMPT = b('password') SUDO_OPTIONS = [ #(False, 'bool', '--askpass', '-A') @@ -170,11 +239,15 @@ class Stream(mitogen.parent.Stream): password_sent = False for buf in it: - LOG.debug('%r: received %r', self, buf) + LOG.debug('%s: received %r', self.name, buf) if buf.endswith(self.EC0_MARKER): self._ec0_received() return - elif PASSWORD_PROMPT in buf.lower(): + + match = PASSWORD_PROMPT_RE.search(buf.decode('utf-8').lower()) + if match is not None: + LOG.debug('%s: matched password prompt %r', + self.name, match.group(0)) if self.password is None: raise PasswordError(self.password_required_msg) if password_sent: diff --git a/tests/sudo_test.py b/tests/sudo_test.py index 87a13cf9..5bf9f4de 100644 --- a/tests/sudo_test.py +++ b/tests/sudo_test.py @@ -56,5 +56,46 @@ class ConstructorTest(testlib.RouterMixin, testlib.TestCase): ]) +class NonEnglishPromptTest(testlib.DockerMixin, testlib.TestCase): + # Only mitogen/debian-test has a properly configured sudo. + mitogen_test_distro = 'debian' + + def test_password_required(self): + ssh = self.docker_ssh( + username='mitogen__has_sudo', + password='has_sudo_password', + ) + ssh.call(os.putenv, 'LANGUAGE', 'fr') + ssh.call(os.putenv, 'LC_ALL', 'fr_FR.UTF-8') + e = self.assertRaises(mitogen.core.StreamError, + lambda: self.router.sudo(via=ssh) + ) + self.assertTrue(mitogen.sudo.Stream.password_required_msg in str(e)) + + def test_password_incorrect(self): + ssh = self.docker_ssh( + username='mitogen__has_sudo', + password='has_sudo_password', + ) + ssh.call(os.putenv, 'LANGUAGE', 'fr') + ssh.call(os.putenv, 'LC_ALL', 'fr_FR.UTF-8') + e = self.assertRaises(mitogen.core.StreamError, + lambda: self.router.sudo(via=ssh, password='x') + ) + self.assertTrue(mitogen.sudo.Stream.password_incorrect_msg in str(e)) + + def test_password_okay(self): + ssh = self.docker_ssh( + username='mitogen__has_sudo', + password='has_sudo_password', + ) + ssh.call(os.putenv, 'LANGUAGE', 'fr') + ssh.call(os.putenv, 'LC_ALL', 'fr_FR.UTF-8') + e = self.assertRaises(mitogen.core.StreamError, + lambda: self.router.sudo(via=ssh, password='rootpassword') + ) + self.assertTrue(mitogen.sudo.Stream.password_incorrect_msg in str(e)) + + if __name__ == '__main__': unittest2.main()