1 """An object-oriented interface to .netrc files."""
2
3 # Module and documentation by Eric S. Raymond, 21 Dec 1998
4
5 import os, shlex, stat
6
7 __all__ = ["netrc", "NetrcParseError"]
8
9
10 class ESC[4;38;5;81mNetrcParseError(ESC[4;38;5;149mException):
11 """Exception raised on syntax errors in the .netrc file."""
12 def __init__(self, msg, filename=None, lineno=None):
13 self.filename = filename
14 self.lineno = lineno
15 self.msg = msg
16 Exception.__init__(self, msg)
17
18 def __str__(self):
19 return "%s (%s, line %s)" % (self.msg, self.filename, self.lineno)
20
21
22 class ESC[4;38;5;81m_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
66 class ESC[4;38;5;81mnetrc:
67 def __init__(self, file=None):
68 default_netrc = file is None
69 if file is None:
70 file = os.path.join(os.path.expanduser("~"), ".netrc")
71 self.hosts = {}
72 self.macros = {}
73 try:
74 with open(file, encoding="utf-8") as fp:
75 self._parse(file, fp, default_netrc)
76 except UnicodeDecodeError:
77 with open(file, encoding="locale") as fp:
78 self._parse(file, fp, default_netrc)
79
80 def _parse(self, file, fp, default_netrc):
81 lexer = _netrclex(fp)
82 while 1:
83 # Look for a machine, default, or macdef top-level keyword
84 saved_lineno = lexer.lineno
85 toplevel = tt = lexer.get_token()
86 if not tt:
87 break
88 elif tt[0] == '#':
89 if lexer.lineno == saved_lineno and len(tt) == 1:
90 lexer.instream.readline()
91 continue
92 elif tt == 'machine':
93 entryname = lexer.get_token()
94 elif tt == 'default':
95 entryname = 'default'
96 elif tt == 'macdef':
97 entryname = lexer.get_token()
98 self.macros[entryname] = []
99 while 1:
100 line = lexer.instream.readline()
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.
109 break
110 self.macros[entryname].append(line)
111 continue
112 else:
113 raise NetrcParseError(
114 "bad toplevel token %r" % tt, file, lexer.lineno)
115
116 if not entryname:
117 raise NetrcParseError("missing %r name" % tt, file, lexer.lineno)
118
119 # We're looking at start of an entry for a named machine or default.
120 login = account = password = ''
121 self.hosts[entryname] = {}
122 while 1:
123 prev_lineno = lexer.lineno
124 tt = lexer.get_token()
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
133 elif tt == 'login' or tt == 'user':
134 login = lexer.get_token()
135 elif tt == 'account':
136 account = lexer.get_token()
137 elif tt == 'password':
138 password = lexer.get_token()
139 else:
140 raise NetrcParseError("bad follower token %r" % tt,
141 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")
165
166 def authenticators(self, host):
167 """Return a (user, account, password) tuple for given host."""
168 if host in self.hosts:
169 return self.hosts[host]
170 elif 'default' in self.hosts:
171 return self.hosts['default']
172 else:
173 return None
174
175 def __repr__(self):
176 """Dump the class data in the format of a .netrc file."""
177 rep = ""
178 for host in self.hosts.keys():
179 attrs = self.hosts[host]
180 rep += f"machine {host}\n\tlogin {attrs[0]}\n"
181 if attrs[1]:
182 rep += f"\taccount {attrs[1]}\n"
183 rep += f"\tpassword {attrs[2]}\n"
184 for macro in self.macros.keys():
185 rep += f"macdef {macro}\n"
186 for line in self.macros[macro]:
187 rep += line
188 rep += "\n"
189 return rep
190
191 if __name__ == '__main__':
192 print(netrc())