mirror of
https://github.com/python/cpython.git
synced 2025-08-04 00:48:58 +00:00
#18116: backport fix to 3.3 since real-world failure mode demonstrated.
In issue 20074 it was pointed out that getpass would fail with a traceback if stdin was, for example /dev/null, which is a non-unlikely scenario. Also backported the tests from issue 17484 as modified by issue 18116. (What I really did was copy getpass.py and test_getpass.py from their state on tip as of 17bd04fbf3d3).
This commit is contained in:
parent
5eb01530b2
commit
ecff5e51a5
4 changed files with 213 additions and 45 deletions
|
@ -15,7 +15,11 @@ On the Mac EasyDialogs.AskPassword is used, if available.
|
|||
# Guido van Rossum (Windows support and cleanup)
|
||||
# Gregory P. Smith (tty support & GetPassWarning)
|
||||
|
||||
import os, sys, warnings
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
__all__ = ["getpass","getuser","GetPassWarning"]
|
||||
|
||||
|
@ -38,52 +42,57 @@ def unix_getpass(prompt='Password: ', stream=None):
|
|||
|
||||
Always restores terminal settings before returning.
|
||||
"""
|
||||
fd = None
|
||||
tty = None
|
||||
try:
|
||||
# Always try reading and writing directly on the tty first.
|
||||
fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
|
||||
tty = os.fdopen(fd, 'w+', 1)
|
||||
input = tty
|
||||
if not stream:
|
||||
stream = tty
|
||||
except EnvironmentError as e:
|
||||
# If that fails, see if stdin can be controlled.
|
||||
passwd = None
|
||||
with contextlib.ExitStack() as stack:
|
||||
try:
|
||||
fd = sys.stdin.fileno()
|
||||
except (AttributeError, ValueError):
|
||||
passwd = fallback_getpass(prompt, stream)
|
||||
input = sys.stdin
|
||||
if not stream:
|
||||
stream = sys.stderr
|
||||
|
||||
if fd is not None:
|
||||
passwd = None
|
||||
try:
|
||||
old = termios.tcgetattr(fd) # a copy to save
|
||||
new = old[:]
|
||||
new[3] &= ~termios.ECHO # 3 == 'lflags'
|
||||
tcsetattr_flags = termios.TCSAFLUSH
|
||||
if hasattr(termios, 'TCSASOFT'):
|
||||
tcsetattr_flags |= termios.TCSASOFT
|
||||
# Always try reading and writing directly on the tty first.
|
||||
fd = os.open('/dev/tty', os.O_RDWR|os.O_NOCTTY)
|
||||
tty = io.FileIO(fd, 'w+')
|
||||
stack.enter_context(tty)
|
||||
input = io.TextIOWrapper(tty)
|
||||
stack.enter_context(input)
|
||||
if not stream:
|
||||
stream = input
|
||||
except OSError as e:
|
||||
# If that fails, see if stdin can be controlled.
|
||||
stack.close()
|
||||
try:
|
||||
termios.tcsetattr(fd, tcsetattr_flags, new)
|
||||
passwd = _raw_input(prompt, stream, input=input)
|
||||
finally:
|
||||
termios.tcsetattr(fd, tcsetattr_flags, old)
|
||||
stream.flush() # issue7208
|
||||
except termios.error:
|
||||
if passwd is not None:
|
||||
# _raw_input succeeded. The final tcsetattr failed. Reraise
|
||||
# instead of leaving the terminal in an unknown state.
|
||||
raise
|
||||
# We can't control the tty or stdin. Give up and use normal IO.
|
||||
# fallback_getpass() raises an appropriate warning.
|
||||
del input, tty # clean up unused file objects before blocking
|
||||
passwd = fallback_getpass(prompt, stream)
|
||||
fd = sys.stdin.fileno()
|
||||
except (AttributeError, ValueError):
|
||||
fd = None
|
||||
passwd = fallback_getpass(prompt, stream)
|
||||
input = sys.stdin
|
||||
if not stream:
|
||||
stream = sys.stderr
|
||||
|
||||
stream.write('\n')
|
||||
return passwd
|
||||
if fd is not None:
|
||||
try:
|
||||
old = termios.tcgetattr(fd) # a copy to save
|
||||
new = old[:]
|
||||
new[3] &= ~termios.ECHO # 3 == 'lflags'
|
||||
tcsetattr_flags = termios.TCSAFLUSH
|
||||
if hasattr(termios, 'TCSASOFT'):
|
||||
tcsetattr_flags |= termios.TCSASOFT
|
||||
try:
|
||||
termios.tcsetattr(fd, tcsetattr_flags, new)
|
||||
passwd = _raw_input(prompt, stream, input=input)
|
||||
finally:
|
||||
termios.tcsetattr(fd, tcsetattr_flags, old)
|
||||
stream.flush() # issue7208
|
||||
except termios.error:
|
||||
if passwd is not None:
|
||||
# _raw_input succeeded. The final tcsetattr failed. Reraise
|
||||
# instead of leaving the terminal in an unknown state.
|
||||
raise
|
||||
# We can't control the tty or stdin. Give up and use normal IO.
|
||||
# fallback_getpass() raises an appropriate warning.
|
||||
if stream is not input:
|
||||
# clean up unused file objects before blocking
|
||||
stack.close()
|
||||
passwd = fallback_getpass(prompt, stream)
|
||||
|
||||
stream.write('\n')
|
||||
return passwd
|
||||
|
||||
|
||||
def win_getpass(prompt='Password: ', stream=None):
|
||||
|
|
155
Lib/test/test_getpass.py
Normal file
155
Lib/test/test_getpass.py
Normal file
|
@ -0,0 +1,155 @@
|
|||
import getpass
|
||||
import os
|
||||
import unittest
|
||||
from io import BytesIO, StringIO
|
||||
from unittest import mock
|
||||
from test import support
|
||||
|
||||
try:
|
||||
import termios
|
||||
except ImportError:
|
||||
termios = None
|
||||
try:
|
||||
import pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
|
||||
@mock.patch('os.environ')
|
||||
class GetpassGetuserTest(unittest.TestCase):
|
||||
|
||||
def test_username_takes_username_from_env(self, environ):
|
||||
expected_name = 'some_name'
|
||||
environ.get.return_value = expected_name
|
||||
self.assertEqual(expected_name, getpass.getuser())
|
||||
|
||||
def test_username_priorities_of_env_values(self, environ):
|
||||
environ.get.return_value = None
|
||||
try:
|
||||
getpass.getuser()
|
||||
except ImportError: # in case there's no pwd module
|
||||
pass
|
||||
self.assertEqual(
|
||||
environ.get.call_args_list,
|
||||
[mock.call(x) for x in ('LOGNAME', 'USER', 'LNAME', 'USERNAME')])
|
||||
|
||||
def test_username_falls_back_to_pwd(self, environ):
|
||||
expected_name = 'some_name'
|
||||
environ.get.return_value = None
|
||||
if pwd:
|
||||
with mock.patch('os.getuid') as uid, \
|
||||
mock.patch('pwd.getpwuid') as getpw:
|
||||
uid.return_value = 42
|
||||
getpw.return_value = [expected_name]
|
||||
self.assertEqual(expected_name,
|
||||
getpass.getuser())
|
||||
getpw.assert_called_once_with(42)
|
||||
else:
|
||||
self.assertRaises(ImportError, getpass.getuser)
|
||||
|
||||
|
||||
class GetpassRawinputTest(unittest.TestCase):
|
||||
|
||||
def test_flushes_stream_after_prompt(self):
|
||||
# see issue 1703
|
||||
stream = mock.Mock(spec=StringIO)
|
||||
input = StringIO('input_string')
|
||||
getpass._raw_input('some_prompt', stream, input=input)
|
||||
stream.flush.assert_called_once_with()
|
||||
|
||||
def test_uses_stderr_as_default(self):
|
||||
input = StringIO('input_string')
|
||||
prompt = 'some_prompt'
|
||||
with mock.patch('sys.stderr') as stderr:
|
||||
getpass._raw_input(prompt, input=input)
|
||||
stderr.write.assert_called_once_with(prompt)
|
||||
|
||||
@mock.patch('sys.stdin')
|
||||
def test_uses_stdin_as_default_input(self, mock_input):
|
||||
mock_input.readline.return_value = 'input_string'
|
||||
getpass._raw_input(stream=StringIO())
|
||||
mock_input.readline.assert_called_once_with()
|
||||
|
||||
def test_raises_on_empty_input(self):
|
||||
input = StringIO('')
|
||||
self.assertRaises(EOFError, getpass._raw_input, input=input)
|
||||
|
||||
def test_trims_trailing_newline(self):
|
||||
input = StringIO('test\n')
|
||||
self.assertEqual('test', getpass._raw_input(input=input))
|
||||
|
||||
|
||||
# Some of these tests are a bit white-box. The functional requirement is that
|
||||
# the password input be taken directly from the tty, and that it not be echoed
|
||||
# on the screen, unless we are falling back to stderr/stdin.
|
||||
|
||||
# Some of these might run on platforms without termios, but play it safe.
|
||||
@unittest.skipUnless(termios, 'tests require system with termios')
|
||||
class UnixGetpassTest(unittest.TestCase):
|
||||
|
||||
def test_uses_tty_directly(self):
|
||||
with mock.patch('os.open') as open, \
|
||||
mock.patch('io.FileIO') as fileio, \
|
||||
mock.patch('io.TextIOWrapper') as textio:
|
||||
# By setting open's return value to None the implementation will
|
||||
# skip code we don't care about in this test. We can mock this out
|
||||
# fully if an alternate implementation works differently.
|
||||
open.return_value = None
|
||||
getpass.unix_getpass()
|
||||
open.assert_called_once_with('/dev/tty',
|
||||
os.O_RDWR | os.O_NOCTTY)
|
||||
fileio.assert_called_once_with(open.return_value, 'w+')
|
||||
textio.assert_called_once_with(fileio.return_value)
|
||||
|
||||
def test_resets_termios(self):
|
||||
with mock.patch('os.open') as open, \
|
||||
mock.patch('io.FileIO'), \
|
||||
mock.patch('io.TextIOWrapper'), \
|
||||
mock.patch('termios.tcgetattr') as tcgetattr, \
|
||||
mock.patch('termios.tcsetattr') as tcsetattr:
|
||||
open.return_value = 3
|
||||
fake_attrs = [255, 255, 255, 255, 255]
|
||||
tcgetattr.return_value = list(fake_attrs)
|
||||
getpass.unix_getpass()
|
||||
tcsetattr.assert_called_with(3, mock.ANY, fake_attrs)
|
||||
|
||||
def test_falls_back_to_fallback_if_termios_raises(self):
|
||||
with mock.patch('os.open') as open, \
|
||||
mock.patch('io.FileIO') as fileio, \
|
||||
mock.patch('io.TextIOWrapper') as textio, \
|
||||
mock.patch('termios.tcgetattr'), \
|
||||
mock.patch('termios.tcsetattr') as tcsetattr, \
|
||||
mock.patch('getpass.fallback_getpass') as fallback:
|
||||
open.return_value = 3
|
||||
fileio.return_value = BytesIO()
|
||||
tcsetattr.side_effect = termios.error
|
||||
getpass.unix_getpass()
|
||||
fallback.assert_called_once_with('Password: ',
|
||||
textio.return_value)
|
||||
|
||||
def test_flushes_stream_after_input(self):
|
||||
# issue 7208
|
||||
with mock.patch('os.open') as open, \
|
||||
mock.patch('io.FileIO'), \
|
||||
mock.patch('io.TextIOWrapper'), \
|
||||
mock.patch('termios.tcgetattr'), \
|
||||
mock.patch('termios.tcsetattr'):
|
||||
open.return_value = 3
|
||||
mock_stream = mock.Mock(spec=StringIO)
|
||||
getpass.unix_getpass(stream=mock_stream)
|
||||
mock_stream.flush.assert_called_with()
|
||||
|
||||
def test_falls_back_to_stdin(self):
|
||||
with mock.patch('os.open') as os_open, \
|
||||
mock.patch('sys.stdin', spec=StringIO) as stdin:
|
||||
os_open.side_effect = IOError
|
||||
stdin.fileno.side_effect = AttributeError
|
||||
with support.captured_stderr() as stderr:
|
||||
with self.assertWarns(getpass.GetPassWarning):
|
||||
getpass.unix_getpass()
|
||||
stdin.readline.assert_called_once_with()
|
||||
self.assertIn('Warning', stderr.getvalue())
|
||||
self.assertIn('Password:', stderr.getvalue())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -41,7 +41,6 @@ class TestUntestedModules(unittest.TestCase):
|
|||
|
||||
import encodings
|
||||
import formatter
|
||||
import getpass
|
||||
import html.entities
|
||||
import imghdr
|
||||
import keyword
|
||||
|
|
|
@ -29,6 +29,11 @@ Core and Builtins
|
|||
Library
|
||||
-------
|
||||
|
||||
- Issue #18116: getpass was always getting an error when testing /dev/tty,
|
||||
and thus was always falling back to stdin, and would then raise an exception
|
||||
if stdin could not be used (such as /dev/null). It also leaked an open file.
|
||||
All of these issues are now fixed.
|
||||
|
||||
- Issue #20027: Fixed locale aliases for devanagari locales.
|
||||
|
||||
- Issue #20067: Tkinter variables now work when wantobjects is false.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue