diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst index 64aa3ac7c8aefd..1bfd7f3fad9fdd 100644 --- a/Doc/library/netrc.rst +++ b/Doc/library/netrc.rst @@ -18,7 +18,7 @@ the Unix :program:`ftp` program and other FTP clients. .. class:: netrc([file]) - A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc + A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc file. The initialization argument, if present, specifies the file to parse. If no argument is given, the file :file:`.netrc` in the user's home directory will be read. Parse errors will raise :exc:`NetrcParseError` with diagnostic @@ -32,6 +32,12 @@ the Unix :program:`ftp` program and other FTP clients. .. versionchanged:: 3.4 Added the POSIX permission check. + .. versionchanged:: 3.7 + The entry in the netrc file no longer needs to contain all tokens. The missing + tokens' value default to an empty string. All the tokens and their values now + can contain arbitrary characters, like whitespace and non-ASCII characters. + If the login name is anonymous, it won't trigger the security check. + .. exception:: NetrcParseError @@ -75,11 +81,3 @@ Instances of :class:`~netrc.netrc` have public instance variables: .. attribute:: netrc.macros Dictionary mapping macro names to string lists. - -.. note:: - - Passwords are limited to a subset of the ASCII character set. All ASCII - punctuation is allowed in passwords, however, note that whitespace and - non-printable characters are not allowed in passwords. This is a limitation - of the way the .netrc file is parsed and may be removed in the future. - diff --git a/Lib/netrc.py b/Lib/netrc.py index bbb3d23b543343..f8e7b25f826049 100644 --- a/Lib/netrc.py +++ b/Lib/netrc.py @@ -19,6 +19,51 @@ def __str__(self): return "%s (%s, line %s)" % (self.msg, self.filename, self.lineno) +class _netrclex: + def __init__(self, fp): + self.lineno = 1 + self.instream = fp + self.whitespace = "\n\t\r " + self._stack = [] + + def _read_char(self): + ch = self.instream.read(1) + if ch == "\n": + self.lineno += 1 + return ch + + def get_token(self): + if self._stack: + return self._stack.pop(0) + token = "" + fiter = iter(self._read_char, "") + for ch in fiter: + if ch in self.whitespace: + continue + if ch == "\"": + for ch in fiter: + if ch != "\"": + if ch == "\\": + ch = self._read_char() + token += ch + continue + return token + else: + if ch == "\\": + ch = self._read_char() + token += ch + for ch in fiter: + if ch not in self.whitespace: + if ch == "\\": + ch = self._read_char() + token += ch + continue + return token + return token + + def push_token(self, token): + self._stack.append(token) + class netrc: def __init__(self, file=None): default_netrc = file is None @@ -33,31 +78,31 @@ def __init__(self, file=None): self._parse(file, fp, default_netrc) def _parse(self, file, fp, default_netrc): - lexer = shlex.shlex(fp) - lexer.wordchars += r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" - lexer.commenters = lexer.commenters.replace('#', '') + lexer = _netrclex(fp) while 1: # Look for a machine, default, or macdef top-level keyword - saved_lineno = lexer.lineno - toplevel = tt = lexer.get_token() + prev_lineno = lexer.lineno + tt = lexer.get_token() if not tt: break elif tt[0] == '#': - if lexer.lineno == saved_lineno and len(tt) == 1: + if prev_lineno == lexer.lineno: lexer.instream.readline() continue elif tt == 'machine': entryname = lexer.get_token() elif tt == 'default': entryname = 'default' - elif tt == 'macdef': # Just skip to end of macdefs + elif tt == 'macdef': entryname = lexer.get_token() self.macros[entryname] = [] - lexer.whitespace = ' \t' while 1: line = lexer.instream.readline() - if not line or line == '\012': - lexer.whitespace = ' \t\r\n' + if not line: + raise NetrcParseError( + "Macro definition missing null line terminator.", + file, lexer.lineno) + if line == '\n': break self.macros[entryname].append(line) continue @@ -65,29 +110,29 @@ def _parse(self, file, fp, default_netrc): raise NetrcParseError( "bad toplevel token %r" % tt, file, lexer.lineno) + if not entryname: + raise NetrcParseError("missing %r name" % tt, file, lexer.lineno) + # We're looking at start of an entry for a named machine or default. - login = '' - account = password = None + login = account = password = '' self.hosts[entryname] = {} while 1: + prev_lineno = lexer.lineno tt = lexer.get_token() - if (tt.startswith('#') or - tt in {'', 'machine', 'default', 'macdef'}): - if password: - self.hosts[entryname] = (login, account, password) - lexer.push_token(tt) - break - else: - raise NetrcParseError( - "malformed %s entry %s terminated by %s" - % (toplevel, entryname, repr(tt)), - file, lexer.lineno) + if tt.startswith('#'): + if lexer.lineno == prev_lineno: + lexer.instream.readline() + continue + if tt in {'', 'machine', 'default', 'macdef'}: + self.hosts[entryname] = (login, account, password) + lexer.push_token(tt) + break elif tt == 'login' or tt == 'user': login = lexer.get_token() elif tt == 'account': account = lexer.get_token() elif tt == 'password': - if os.name == 'posix' and default_netrc: + if os.name == 'posix' and default_netrc and login != "anonymous": prop = os.fstat(fp.fileno()) if prop.st_uid != os.getuid(): import pwd @@ -127,10 +172,10 @@ def __repr__(self): rep = "" for host in self.hosts.keys(): attrs = self.hosts[host] - rep = rep + "machine "+ host + "\n\tlogin " + repr(attrs[0]) + "\n" + rep = rep + "machine "+ host + "\n\tlogin " + attrs[0] + "\n" if attrs[1]: - rep = rep + "account " + repr(attrs[1]) - rep = rep + "\tpassword " + repr(attrs[2]) + "\n" + rep = rep + "account " + attrs[1] + rep = rep + "\tpassword " + attrs[2] + "\n" for macro in self.macros.keys(): rep = rep + "macdef " + macro + "\n" for line in self.macros[macro]: diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 60a3ec9565b4cf..d458dcc9018aa8 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -12,20 +12,22 @@ def make_nrc(self, test_data): mode += 't' with open(temp_filename, mode) as fp: fp.write(test_data) - self.addCleanup(os.unlink, temp_filename) - return netrc.netrc(temp_filename) + try: + nrc = netrc.netrc(temp_filename) + finally: + os.unlink(temp_filename) + return nrc - def test_default(self): + def test_toplevel_tokens(self): nrc = self.make_nrc("""\ - machine host1.domain.com login log1 password pass1 account acct1 - default login log2 password pass2 + machine host.domain.com login log1 password pass1 account acct1 + default login log2 password pass2 account acct2 """) - self.assertEqual(nrc.hosts['host1.domain.com'], - ('log1', 'acct1', 'pass1')) - self.assertEqual(nrc.hosts['default'], ('log2', None, 'pass2')) + self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) + self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) def test_macros(self): - nrc = self.make_nrc("""\ + data = """\ macdef macro1 line1 line2 @@ -33,33 +35,151 @@ def test_macros(self): macdef macro2 line3 line4 - """) + + """ + nrc = self.make_nrc(data) self.assertEqual(nrc.macros, {'macro1': ['line1\n', 'line2\n'], 'macro2': ['line3\n', 'line4\n']}) + # strip the last \n + self.assertRaises(netrc.NetrcParseError, self.make_nrc, + data.rstrip(' ')[:-1]) + + def test_optional_tokens(self): + data = ( + "machine host.domain.com", + "machine host.domain.com login", + "machine host.domain.com account", + "machine host.domain.com password", + "machine host.domain.com login \"\" account", + "machine host.domain.com login \"\" password", + "machine host.domain.com account \"\" password" + ) + for item in data: + nrc = self.make_nrc(item) + self.assertEqual(nrc.hosts['host.domain.com'], ('', '', '')) + data = ( + "default", + "default login", + "default account", + "default password", + "default login \"\" account", + "default login \"\" password", + "default account \"\" password" + ) + for item in data: + nrc = self.make_nrc(item) + self.assertEqual(nrc.hosts['default'], ('', '', '')) - def _test_passwords(self, nrc, passwd): + def test_invalid_tokens(self): + data = ( + "invalid host.domain.com", + "machine host.domain.com invalid", + "machine host.domain.com login log password pass account acct invalid", + "default host.domain.com invalid", + "default host.domain.com login log password pass account acct invalid" + ) + for item in data: + self.assertRaises(netrc.NetrcParseError, self.make_nrc, item) + + def _test_token_x(self, nrc, token, value): nrc = self.make_nrc(nrc) - self.assertEqual(nrc.hosts['host.domain.com'], ('log', 'acct', passwd)) + if token == 'login': + self.assertEqual(nrc.hosts['host.domain.com'], (value, 'acct', 'pass')) + elif token == 'account': + self.assertEqual(nrc.hosts['host.domain.com'], ('log', value, 'pass')) + elif token == 'password': + self.assertEqual(nrc.hosts['host.domain.com'], ('log', 'acct', value)) + + def test_token_value_quotes(self): + self._test_token_x("""\ + machine host.domain.com login "log" password pass account acct + """, 'login', 'log') + self._test_token_x("""\ + machine host.domain.com login log password pass account "acct" + """, 'account', 'acct') + self._test_token_x("""\ + machine host.domain.com login log password "pass" account acct + """, 'password', 'pass') + + def test_token_value_escape(self): + self._test_token_x("""\ + machine host.domain.com login \\"log password pass account acct + """, 'login', '"log') + self._test_token_x("""\ + machine host.domain.com login "\\"log" password pass account acct + """, 'login', '"log') + self._test_token_x("""\ + machine host.domain.com login log password pass account \\"acct + """, 'account', '"acct') + self._test_token_x("""\ + machine host.domain.com login log password pass account "\\"acct" + """, 'account', '"acct') + self._test_token_x("""\ + machine host.domain.com login log password \\"pass account acct + """, 'password', '"pass') + self._test_token_x("""\ + machine host.domain.com login log password "\\"pass" account acct + """, 'password', '"pass') - def test_password_with_leading_hash(self): - self._test_passwords("""\ + def test_token_value_whitespace(self): + self._test_token_x("""\ + machine host.domain.com login "lo g" password pass account acct + """, 'login', 'lo g') + self._test_token_x("""\ + machine host.domain.com login log password "pas s" account acct + """, 'password', 'pas s') + self._test_token_x("""\ + machine host.domain.com login log password pass account "acc t" + """, 'account', 'acc t') + + def test_token_value_non_ascii(self): + self._test_token_x("""\ + machine host.domain.com login \xa1\xa2 password pass account acct + """, 'login', '\xa1\xa2') + self._test_token_x("""\ + machine host.domain.com login log password pass account \xa1\xa2 + """, 'account', '\xa1\xa2') + self._test_token_x("""\ + machine host.domain.com login log password \xa1\xa2 account acct + """, 'password', '\xa1\xa2') + + def test_token_value_leading_hash(self): + self._test_token_x("""\ + machine host.domain.com login #log password pass account acct + """, 'login', '#log') + self._test_token_x("""\ + machine host.domain.com login log password pass account #acct + """, 'account', '#acct') + self._test_token_x("""\ machine host.domain.com login log password #pass account acct - """, '#pass') + """, 'password', '#pass') - def test_password_with_trailing_hash(self): - self._test_passwords("""\ + def test_token_value_trailing_hash(self): + self._test_token_x("""\ + machine host.domain.com login log# password pass account acct + """, 'login', 'log#') + self._test_token_x("""\ + machine host.domain.com login log password pass account acct# + """, 'account', 'acct#') + self._test_token_x("""\ machine host.domain.com login log password pass# account acct - """, 'pass#') + """, 'password', 'pass#') - def test_password_with_internal_hash(self): - self._test_passwords("""\ + def test_token_value_internal_hash(self): + self._test_token_x("""\ + machine host.domain.com login lo#g password pass account acct + """, 'login', 'lo#g') + self._test_token_x("""\ + machine host.domain.com login log password pass account ac#ct + """, 'account', 'ac#ct') + self._test_token_x("""\ machine host.domain.com login log password pa#ss account acct - """, 'pa#ss') + """, 'password', 'pa#ss') def _test_comment(self, nrc, passwd='pass'): nrc = self.make_nrc(nrc) - self.assertEqual(nrc.hosts['foo.domain.com'], ('bar', None, passwd)) - self.assertEqual(nrc.hosts['bar.domain.com'], ('foo', None, 'pass')) + self.assertEqual(nrc.hosts['foo.domain.com'], ('bar', '', passwd)) + self.assertEqual(nrc.hosts['bar.domain.com'], ('foo', '', 'pass')) def test_comment_before_machine_line(self): self._test_comment("""\ @@ -82,6 +202,42 @@ def test_comment_before_machine_line_hash_only(self): machine bar.domain.com login foo password pass """) + def test_comment_after_machine_line(self): + self._test_comment("""\ + machine foo.domain.com login bar password pass + # comment + machine bar.domain.com login foo password pass + """) + self._test_comment("""\ + machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + # comment + """) + + def test_comment_after_machine_line_no_space(self): + self._test_comment("""\ + machine foo.domain.com login bar password pass + #comment + machine bar.domain.com login foo password pass + """) + self._test_comment("""\ + machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + #comment + """) + + def test_comment_after_machine_line_hash_only(self): + self._test_comment("""\ + machine foo.domain.com login bar password pass + # + machine bar.domain.com login foo password pass + """) + self._test_comment("""\ + machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + # + """) + def test_comment_at_end_of_machine_line(self): self._test_comment("""\ machine foo.domain.com login bar password pass # comment @@ -119,9 +275,23 @@ def test_security(self): os.chmod(fn, 0o600) nrc = netrc.netrc() self.assertEqual(nrc.hosts['foo.domain.com'], - ('bar', None, 'pass')) + ('bar', '', 'pass')) os.chmod(fn, 0o622) self.assertRaises(netrc.NetrcParseError, netrc.netrc) + with open(fn, 'wt') as f: + f.write("""\ + machine foo.domain.com login anonymous password pass + default login foo password pass + """) + with support.EnvironmentVarGuard() as environ: + environ.set('HOME', d) + os.chmod(fn, 0o600) + nrc = netrc.netrc() + self.assertEqual(nrc.hosts['foo.domain.com'], + ('anonymous', '', 'pass')) + os.chmod(fn, 0o622) + self.assertEqual(nrc.hosts['foo.domain.com'], + ('anonymous', '', 'pass')) def test_main(): support.run_unittest(NetrcTestCase)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: