Skip to content

Commit 15409c7

Browse files
bpo-28806: Continue work: improve the netrc library (GH-26330)
Continue with the improvement of the library netrc Original work and report Xiang Zhang <angwerzx@126.com> * πŸ“œπŸ€– Added by blurb_it. Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
1 parent da20d74 commit 15409c7

File tree

4 files changed

+319
-129
lines changed

4 files changed

+319
-129
lines changed

β€ŽDoc/library/netrc.rst

+4-7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ the Unix :program:`ftp` program and other FTP clients.
4141
.. versionchanged:: 3.10
4242
:class:`netrc` try UTF-8 encoding before using locale specific
4343
encoding.
44+
The entry in the netrc file no longer needs to contain all tokens. The missing
45+
tokens' value default to an empty string. All the tokens and their values now
46+
can contain arbitrary characters, like whitespace and non-ASCII characters.
47+
If the login name is anonymous, it won't trigger the security check.
4448

4549

4650
.. exception:: NetrcParseError
@@ -85,10 +89,3 @@ Instances of :class:`~netrc.netrc` have public instance variables:
8589
.. attribute:: netrc.macros
8690

8791
Dictionary mapping macro names to string lists.
88-
89-
.. note::
90-
91-
Passwords are limited to a subset of the ASCII character set. All ASCII
92-
punctuation is allowed in passwords, however, note that whitespace and
93-
non-printable characters are not allowed in passwords. This is a limitation
94-
of the way the .netrc file is parsed and may be removed in the future.

β€ŽLib/netrc.py

+90-41
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,50 @@ def __str__(self):
1919
return "%s (%s, line %s)" % (self.msg, self.filename, self.lineno)
2020

2121

22+
class _netrclex:
23+
def __init__(self, fp):
24+
self.lineno = 1
25+
self.instream = fp
26+
self.whitespace = "\n\t\r "
27+
self.pushback = []
28+
29+
def _read_char(self):
30+
ch = self.instream.read(1)
31+
if ch == "\n":
32+
self.lineno += 1
33+
return ch
34+
35+
def get_token(self):
36+
if self.pushback:
37+
return self.pushback.pop(0)
38+
token = ""
39+
fiter = iter(self._read_char, "")
40+
for ch in fiter:
41+
if ch in self.whitespace:
42+
continue
43+
if ch == '"':
44+
for ch in fiter:
45+
if ch == '"':
46+
return token
47+
elif ch == "\\":
48+
ch = self._read_char()
49+
token += ch
50+
else:
51+
if ch == "\\":
52+
ch = self._read_char()
53+
token += ch
54+
for ch in fiter:
55+
if ch in self.whitespace:
56+
return token
57+
elif ch == "\\":
58+
ch = self._read_char()
59+
token += ch
60+
return token
61+
62+
def push_token(self, token):
63+
self.pushback.append(token)
64+
65+
2266
class netrc:
2367
def __init__(self, file=None):
2468
default_netrc = file is None
@@ -34,9 +78,7 @@ def __init__(self, file=None):
3478
self._parse(file, fp, default_netrc)
3579

3680
def _parse(self, file, fp, default_netrc):
37-
lexer = shlex.shlex(fp)
38-
lexer.wordchars += r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
39-
lexer.commenters = lexer.commenters.replace('#', '')
81+
lexer = _netrclex(fp)
4082
while 1:
4183
# Look for a machine, default, or macdef top-level keyword
4284
saved_lineno = lexer.lineno
@@ -51,68 +93,75 @@ def _parse(self, file, fp, default_netrc):
5193
entryname = lexer.get_token()
5294
elif tt == 'default':
5395
entryname = 'default'
54-
elif tt == 'macdef': # Just skip to end of macdefs
96+
elif tt == 'macdef':
5597
entryname = lexer.get_token()
5698
self.macros[entryname] = []
57-
lexer.whitespace = ' \t'
5899
while 1:
59100
line = lexer.instream.readline()
60-
if not line or line == '\012':
61-
lexer.whitespace = ' \t\r\n'
101+
if not line:
102+
raise NetrcParseError(
103+
"Macro definition missing null line terminator.",
104+
file, lexer.lineno)
105+
if line == '\n':
106+
# a macro definition finished with consecutive new-line
107+
# characters. The first \n is encountered by the
108+
# readline() method and this is the second \n.
62109
break
63110
self.macros[entryname].append(line)
64111
continue
65112
else:
66113
raise NetrcParseError(
67114
"bad toplevel token %r" % tt, file, lexer.lineno)
68115

116+
if not entryname:
117+
raise NetrcParseError("missing %r name" % tt, file, lexer.lineno)
118+
69119
# We're looking at start of an entry for a named machine or default.
70-
login = ''
71-
account = password = None
120+
login = account = password = ''
72121
self.hosts[entryname] = {}
73122
while 1:
123+
prev_lineno = lexer.lineno
74124
tt = lexer.get_token()
75-
if (tt.startswith('#') or
76-
tt in {'', 'machine', 'default', 'macdef'}):
77-
if password:
78-
self.hosts[entryname] = (login, account, password)
79-
lexer.push_token(tt)
80-
break
81-
else:
82-
raise NetrcParseError(
83-
"malformed %s entry %s terminated by %s"
84-
% (toplevel, entryname, repr(tt)),
85-
file, lexer.lineno)
125+
if tt.startswith('#'):
126+
if lexer.lineno == prev_lineno:
127+
lexer.instream.readline()
128+
continue
129+
if tt in {'', 'machine', 'default', 'macdef'}:
130+
self.hosts[entryname] = (login, account, password)
131+
lexer.push_token(tt)
132+
break
86133
elif tt == 'login' or tt == 'user':
87134
login = lexer.get_token()
88135
elif tt == 'account':
89136
account = lexer.get_token()
90137
elif tt == 'password':
91-
if os.name == 'posix' and default_netrc:
92-
prop = os.fstat(fp.fileno())
93-
if prop.st_uid != os.getuid():
94-
import pwd
95-
try:
96-
fowner = pwd.getpwuid(prop.st_uid)[0]
97-
except KeyError:
98-
fowner = 'uid %s' % prop.st_uid
99-
try:
100-
user = pwd.getpwuid(os.getuid())[0]
101-
except KeyError:
102-
user = 'uid %s' % os.getuid()
103-
raise NetrcParseError(
104-
("~/.netrc file owner (%s) does not match"
105-
" current user (%s)") % (fowner, user),
106-
file, lexer.lineno)
107-
if (prop.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
108-
raise NetrcParseError(
109-
"~/.netrc access too permissive: access"
110-
" permissions must restrict access to only"
111-
" the owner", file, lexer.lineno)
112138
password = lexer.get_token()
113139
else:
114140
raise NetrcParseError("bad follower token %r" % tt,
115141
file, lexer.lineno)
142+
self._security_check(fp, default_netrc, self.hosts[entryname][0])
143+
144+
def _security_check(self, fp, default_netrc, login):
145+
if os.name == 'posix' and default_netrc and login != "anonymous":
146+
prop = os.fstat(fp.fileno())
147+
if prop.st_uid != os.getuid():
148+
import pwd
149+
try:
150+
fowner = pwd.getpwuid(prop.st_uid)[0]
151+
except KeyError:
152+
fowner = 'uid %s' % prop.st_uid
153+
try:
154+
user = pwd.getpwuid(os.getuid())[0]
155+
except KeyError:
156+
user = 'uid %s' % os.getuid()
157+
raise NetrcParseError(
158+
(f"~/.netrc file owner ({fowner}, {user}) does not match"
159+
" current user"))
160+
if (prop.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
161+
raise NetrcParseError(
162+
"~/.netrc access too permissive: access"
163+
" permissions must restrict access to only"
164+
" the owner")
116165

117166
def authenticators(self, host):
118167
"""Return a (user, account, password) tuple for given host."""

0 commit comments

Comments
Β (0)