Skip to content

Commit 601b3f7

Browse files
committed
Improve netrc library largely
1 parent 1d4601c commit 601b3f7

File tree

3 files changed

+273
-60
lines changed

3 files changed

+273
-60
lines changed

Doc/library/netrc.rst

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ the Unix :program:`ftp` program and other FTP clients.
1818

1919
.. class:: netrc([file])
2020

21-
A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc
21+
A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc
2222
file. The initialization argument, if present, specifies the file to parse. If
2323
no argument is given, the file :file:`.netrc` in the user's home directory will
2424
be read. Parse errors will raise :exc:`NetrcParseError` with diagnostic
@@ -32,6 +32,12 @@ the Unix :program:`ftp` program and other FTP clients.
3232

3333
.. versionchanged:: 3.4 Added the POSIX permission check.
3434

35+
.. versionchanged:: 3.7
36+
The entry in the netrc file no longer needs to contain all tokens. The missing
37+
tokens' value default to an empty string. All the tokens and their values now
38+
can contain arbitrary characters, like whitespace and non-ASCII characters.
39+
If the login name is anonymous, it won't trigger the security check.
40+
3541

3642
.. exception:: NetrcParseError
3743

@@ -75,11 +81,3 @@ Instances of :class:`~netrc.netrc` have public instance variables:
7581
.. attribute:: netrc.macros
7682

7783
Dictionary mapping macro names to string lists.
78-
79-
.. note::
80-
81-
Passwords are limited to a subset of the ASCII character set. All ASCII
82-
punctuation is allowed in passwords, however, note that whitespace and
83-
non-printable characters are not allowed in passwords. This is a limitation
84-
of the way the .netrc file is parsed and may be removed in the future.
85-

Lib/netrc.py

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,51 @@ 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._stack = []
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._stack:
37+
return self._stack.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+
if ch == "\\":
47+
ch = self._read_char()
48+
token += ch
49+
continue
50+
return token
51+
else:
52+
if ch == "\\":
53+
ch = self._read_char()
54+
token += ch
55+
for ch in fiter:
56+
if ch not in self.whitespace:
57+
if ch == "\\":
58+
ch = self._read_char()
59+
token += ch
60+
continue
61+
return token
62+
return token
63+
64+
def push_token(self, token):
65+
self._stack.append(token)
66+
2267
class netrc:
2368
def __init__(self, file=None):
2469
default_netrc = file is None
@@ -33,61 +78,61 @@ def __init__(self, file=None):
3378
self._parse(file, fp, default_netrc)
3479

3580
def _parse(self, file, fp, default_netrc):
36-
lexer = shlex.shlex(fp)
37-
lexer.wordchars += r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
38-
lexer.commenters = lexer.commenters.replace('#', '')
81+
lexer = _netrclex(fp)
3982
while 1:
4083
# Look for a machine, default, or macdef top-level keyword
41-
saved_lineno = lexer.lineno
42-
toplevel = tt = lexer.get_token()
84+
prev_lineno = lexer.lineno
85+
tt = lexer.get_token()
4386
if not tt:
4487
break
4588
elif tt[0] == '#':
46-
if lexer.lineno == saved_lineno and len(tt) == 1:
89+
if prev_lineno == lexer.lineno:
4790
lexer.instream.readline()
4891
continue
4992
elif tt == 'machine':
5093
entryname = lexer.get_token()
5194
elif tt == 'default':
5295
entryname = 'default'
53-
elif tt == 'macdef': # Just skip to end of macdefs
96+
elif tt == 'macdef':
5497
entryname = lexer.get_token()
5598
self.macros[entryname] = []
56-
lexer.whitespace = ' \t'
5799
while 1:
58100
line = lexer.instream.readline()
59-
if not line or line == '\012':
60-
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':
61106
break
62107
self.macros[entryname].append(line)
63108
continue
64109
else:
65110
raise NetrcParseError(
66111
"bad toplevel token %r" % tt, file, lexer.lineno)
67112

113+
if not entryname:
114+
raise NetrcParseError("missing %r name" % tt, file, lexer.lineno)
115+
68116
# We're looking at start of an entry for a named machine or default.
69-
login = ''
70-
account = password = None
117+
login = account = password = ''
71118
self.hosts[entryname] = {}
72119
while 1:
120+
prev_lineno = lexer.lineno
73121
tt = lexer.get_token()
74-
if (tt.startswith('#') or
75-
tt in {'', 'machine', 'default', 'macdef'}):
76-
if password:
77-
self.hosts[entryname] = (login, account, password)
78-
lexer.push_token(tt)
79-
break
80-
else:
81-
raise NetrcParseError(
82-
"malformed %s entry %s terminated by %s"
83-
% (toplevel, entryname, repr(tt)),
84-
file, lexer.lineno)
122+
if tt.startswith('#'):
123+
if lexer.lineno == prev_lineno:
124+
lexer.instream.readline()
125+
continue
126+
if tt in {'', 'machine', 'default', 'macdef'}:
127+
self.hosts[entryname] = (login, account, password)
128+
lexer.push_token(tt)
129+
break
85130
elif tt == 'login' or tt == 'user':
86131
login = lexer.get_token()
87132
elif tt == 'account':
88133
account = lexer.get_token()
89134
elif tt == 'password':
90-
if os.name == 'posix' and default_netrc:
135+
if os.name == 'posix' and default_netrc and login != "anonymous":
91136
prop = os.fstat(fp.fileno())
92137
if prop.st_uid != os.getuid():
93138
import pwd
@@ -127,10 +172,10 @@ def __repr__(self):
127172
rep = ""
128173
for host in self.hosts.keys():
129174
attrs = self.hosts[host]
130-
rep = rep + "machine "+ host + "\n\tlogin " + repr(attrs[0]) + "\n"
175+
rep = rep + "machine "+ host + "\n\tlogin " + attrs[0] + "\n"
131176
if attrs[1]:
132-
rep = rep + "account " + repr(attrs[1])
133-
rep = rep + "\tpassword " + repr(attrs[2]) + "\n"
177+
rep = rep + "account " + attrs[1]
178+
rep = rep + "\tpassword " + attrs[2] + "\n"
134179
for macro in self.macros.keys():
135180
rep = rep + "macdef " + macro + "\n"
136181
for line in self.macros[macro]:

0 commit comments

Comments
 (0)