Skip to content

Commit 3666589

Browse files
eamanublurb-it[bot]
authored andcommitted
bpo-28806: Continue work: improve the netrc library (pythonGH-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 5f4ffb4 commit 3666589

File tree

4 files changed

+319
-129
lines changed

4 files changed

+319
-129
lines changed

Doc/library/netrc.rst

Lines changed: 4 additions & 7 deletions
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

Lines changed: 90 additions & 41 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy