##// END OF EJS Templates
python 3 compatible
Victor NOEL -
r62:921d3d72 default
parent child Browse files
Show More
@@ -1,380 +1,380 b''
1 1 # -*- coding: utf-8 -*-
2 2 #
3 3 # mercurial_keyring: save passwords in password database
4 4 #
5 5 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 6 #
7 7 # This software may be used and distributed according to the terms
8 8 # of the GNU General Public License, incorporated herein by reference.
9 9 #
10 10 # See README.txt for more details.
11 11
12 12 from mercurial import hg, repo, util
13 13 from mercurial.i18n import _
14 14 try:
15 15 from mercurial.url import passwordmgr
16 16 except:
17 17 from mercurial.httprepo import passwordmgr
18 18 from mercurial.httprepo import httprepository
19 19 from mercurial import mail
20 20
21 21 import keyring
22 22 from urlparse import urlparse
23 23 import urllib2
24 24 import smtplib, socket
25 25 import os
26 26
27 27 KEYRING_SERVICE = "Mercurial"
28 28
29 29 ############################################################
30 30
31 31 def monkeypatch_method(cls):
32 32 def decorator(func):
33 33 setattr(cls, func.__name__, func)
34 34 return func
35 35 return decorator
36 36
37 37 ############################################################
38 38
39 39 class PasswordStore(object):
40 40 """
41 41 Helper object handling keyring usage (password save&restore,
42 42 the way passwords are keyed in the keyring).
43 43 """
44 44 def __init__(self):
45 45 self.cache = dict()
46 46 def get_http_password(self, url, username):
47 47 return keyring.get_password(KEYRING_SERVICE,
48 48 self._format_http_key(url, username))
49 49 def set_http_password(self, url, username, password):
50 50 keyring.set_password(KEYRING_SERVICE,
51 51 self._format_http_key(url, username),
52 52 password)
53 53 def clear_http_password(self, url, username):
54 54 self.set_http_password(url, username, "")
55 55 def _format_http_key(self, url, username):
56 56 return "%s@@%s" % (username, url)
57 57 def get_smtp_password(self, machine, port, username):
58 58 return keyring.get_password(
59 59 KEYRING_SERVICE,
60 60 self._format_smtp_key(machine, port, username))
61 61 def set_smtp_password(self, machine, port, username, password):
62 62 keyring.set_password(
63 63 KEYRING_SERVICE,
64 64 self._format_smtp_key(machine, port, username),
65 65 password)
66 66 def clear_smtp_password(self, machine, port, username):
67 67 self.set_smtp_password(url, username, "")
68 68 def _format_smtp_key(self, machine, port, username):
69 69 return "%s@@%s:%s" % (username, machine, str(port))
70 70
71 71 password_store = PasswordStore()
72 72
73 73 ############################################################
74 74
75 75 class HTTPPasswordHandler(object):
76 76 """
77 77 Actual implementation of password handling (user prompting,
78 78 configuration file searching, keyring save&restore).
79 79
80 80 Object of this class is bound as passwordmgr attribute.
81 81 """
82 82 def __init__(self):
83 83 self.pwd_cache = {}
84 84 self.last_reply = None
85 85
86 86 def find_auth(self, pwmgr, realm, authuri):
87 87 """
88 88 Actual implementation of find_user_password - different
89 89 ways of obtaining the username and password.
90 90 """
91 91 ui = pwmgr.ui
92 92
93 93 # If we are called again just after identical previous
94 94 # request, then the previously returned auth must have been
95 95 # wrong. So we note this to force password prompt (and avoid
96 96 # reusing bad password indifinitely).
97 97 after_bad_auth = (self.last_reply \
98 98 and (self.last_reply['realm'] == realm) \
99 99 and (self.last_reply['authuri'] == authuri))
100 100
101 101 # Strip arguments to get actual remote repository url.
102 102 base_url = self.canonical_url(authuri)
103 103
104 104 # Extracting possible username (or password)
105 105 # stored directly in repository url
106 106 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
107 107 pwmgr, realm, authuri)
108 108 if user and pwd:
109 109 self._debug_reply(ui, _("Auth data found in repository URL"),
110 110 base_url, user, pwd)
111 111 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
112 112 return user, pwd
113 113
114 114 # Loading .hg/hgrc [auth] section contents. If prefix is given,
115 115 # it will be used as a key to lookup password in the keyring.
116 116 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url)
117 117 if prefix_url:
118 118 keyring_url = prefix_url
119 119 else:
120 120 keyring_url = base_url
121 121 ui.debug("keyring URL: %s\n" % keyring_url)
122 122
123 123 # Checking the memory cache (there may be many http calls per command)
124 124 cache_key = (realm, keyring_url)
125 125 if not after_bad_auth:
126 126 cached_auth = self.pwd_cache.get(cache_key)
127 127 if cached_auth:
128 128 user, pwd = cached_auth
129 129 self._debug_reply(ui, _("Cached auth data found"),
130 130 base_url, user, pwd)
131 131 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
132 132 return user, pwd
133 133
134 134 if auth_user:
135 135 if user and (user != auth_user):
136 136 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user)))
137 137 user = auth_user
138 138 if pwd:
139 139 self.pwd_cache[cache_key] = user, pwd
140 140 self._debug_reply(ui, _("Auth data set in .hg/hgrc"),
141 141 base_url, user, pwd)
142 142 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
143 143 return user, pwd
144 144 else:
145 145 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
146 146
147 147 # Loading password from keyring.
148 148 # Only if username is known (so we know the key) and we are
149 149 # not after failure (so we don't reuse the bad password).
150 150 if user and not after_bad_auth:
151 151 pwd = password_store.get_http_password(keyring_url, user)
152 152 if pwd:
153 153 self.pwd_cache[cache_key] = user, pwd
154 154 self._debug_reply(ui, _("Keyring password found"),
155 155 base_url, user, pwd)
156 156 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
157 157 return user, pwd
158 158
159 159 # Is the username permanently set?
160 160 fixed_user = (user and True or False)
161 161
162 162 # Last resort: interactive prompt
163 163 if not ui.interactive():
164 164 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
165 165
166 166 if not fixed_user:
167 167 ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n"))
168 168
169 169 ui.write(_("http authorization required\n"))
170 170 ui.status(_("realm: %s\n") % realm)
171 171 if fixed_user:
172 172 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
173 173 else:
174 174 user = ui.prompt(_("user:"), default=None)
175 175 pwd = ui.getpass(_("password: "))
176 176
177 177 if fixed_user:
178 178 # Saving password to the keyring.
179 179 # It is done only if username is permanently set.
180 180 # Otherwise we won't be able to find the password so it
181 181 # does not make much sense to preserve it
182 182 ui.debug("Saving password for %s to keyring\n" % user)
183 183 password_store.set_http_password(keyring_url, user, pwd)
184 184
185 185 # Saving password to the memory cache
186 186 self.pwd_cache[cache_key] = user, pwd
187 187
188 188 self._debug_reply(ui, _("Manually entered password"),
189 189 base_url, user, pwd)
190 190 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
191 191 return user, pwd
192 192
193 193 def load_hgrc_auth(self, ui, base_url):
194 194 """
195 195 Loading [auth] section contents from local .hgrc
196 196
197 197 Returns (username, password, prefix) tuple (every
198 198 element can be None)
199 199 """
200 200 # Theoretically 3 lines below should do:
201 201
202 202 #auth_token = self.readauthtoken(base_url)
203 203 #if auth_token:
204 204 # user, pwd = auth.get('username'), auth.get('password')
205 205
206 206 # Unfortunately they do not work, readauthtoken always return
207 207 # None. Why? Because ui (self.ui of passwordmgr) describes the
208 208 # *remote* repository, so does *not* contain any option from
209 209 # local .hg/hgrc.
210 210
211 211 # TODO: mercurial 1.4.2 is claimed to resolve this problem
212 212 # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f)
213 213 # so since this version workaround implemented below should
214 214 # not be necessary. As it will take some time until people
215 215 # migrate to >= 1.4.2, it would be best to implement
216 216 # workaround conditionally.
217 217
218 218 # Workaround: we recreate the repository object
219 219 repo_root = ui.config("bundle", "mainreporoot")
220 220
221 221 from mercurial.ui import ui as _ui
222 222 local_ui = _ui(ui)
223 223 if repo_root:
224 224 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
225 225 local_passwordmgr = passwordmgr(local_ui)
226 226 auth_token = local_passwordmgr.readauthtoken(base_url)
227 227 if auth_token:
228 228 username = auth_token.get('username')
229 229 password = auth_token.get('password')
230 230 prefix = auth_token.get('prefix')
231 231 shortest_url = self.shortest_url(base_url, prefix)
232 232 return username, password, shortest_url
233 233
234 234 return None, None, None
235 235
236 236 def shortest_url(self, base_url, prefix):
237 237 if not prefix or prefix == '*':
238 238 return base_url
239 239 scheme, hostpath = base_url.split('://', 1)
240 240 p = prefix.split('://', 1)
241 241 if len(p) > 1:
242 242 prefix_host_path = p[1]
243 243 else:
244 244 prefix_host_path = prefix
245 245 shortest_url = scheme + '://' + prefix_host_path
246 246 return shortest_url
247 247
248 248 def canonical_url(self, authuri):
249 249 """
250 250 Strips query params from url. Used to convert urls like
251 251 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
252 252 to
253 253 https://repo.machine.com/repos/apps/module
254 254 """
255 255 parsed_url = urlparse(authuri)
256 256 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
257 257 parsed_url.path)
258 258
259 259 def _debug_reply(self, ui, msg, url, user, pwd):
260 260 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (
261 261 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
262 262
263 263 ############################################################
264 264
265 265 @monkeypatch_method(passwordmgr)
266 266 def find_user_password(self, realm, authuri):
267 267 """
268 268 keyring-based implementation of username/password query
269 269 for HTTP(S) connections
270 270
271 271 Passwords are saved in gnome keyring, OSX/Chain or other platform
272 272 specific storage and keyed by the repository url
273 273 """
274 274 # Extend object attributes
275 275 if not hasattr(self, '_pwd_handler'):
276 276 self._pwd_handler = HTTPPasswordHandler()
277 277
278 278 return self._pwd_handler.find_auth(self, realm, authuri)
279 279
280 280 ############################################################
281 281
282 282 def try_smtp_login(ui, smtp_obj, username, password):
283 283 """
284 284 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
285 285 password.
286 286
287 287 Returns:
288 288 - True if login succeeded
289 289 - False if login failed due to the wrong credentials
290 290
291 291 Throws Abort exception if login failed for any other reason.
292 292
293 293 Immediately returns False if password is empty
294 294 """
295 295 if not password:
296 296 return False
297 297 try:
298 298 ui.note(_('(authenticating to mail server as %s)\n') %
299 299 (username))
300 300 smtp_obj.login(username, password)
301 301 return True
302 except smtplib.SMTPException, inst:
302 except smtplib.SMTPException as inst:
303 303 if inst.smtp_code == 535:
304 304 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
305 305 return False
306 306 else:
307 307 raise util.Abort(inst)
308 308
309 309 def keyring_supported_smtp(ui, username):
310 310 """
311 311 keyring-integrated replacement for mercurial.mail._smtp
312 312 Used only when configuration file contains username, but
313 313 does not contain the password.
314 314
315 315 Most of the routine below is copied as-is from
316 316 mercurial.mail._smtp. The only changed part is
317 317 marked with #>>>>> and #<<<<< markers
318 318 """
319 319 local_hostname = ui.config('smtp', 'local_hostname')
320 320 s = smtplib.SMTP(local_hostname=local_hostname)
321 321 mailhost = ui.config('smtp', 'host')
322 322 if not mailhost:
323 323 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
324 324 mailport = int(ui.config('smtp', 'port', 25))
325 325 ui.note(_('sending mail: smtp host %s, port %s\n') %
326 326 (mailhost, mailport))
327 327 s.connect(host=mailhost, port=mailport)
328 328 if ui.configbool('smtp', 'tls'):
329 329 if not hasattr(socket, 'ssl'):
330 330 raise util.Abort(_("can't use TLS: Python SSL support "
331 331 "not installed"))
332 332 ui.note(_('(using tls)\n'))
333 333 s.ehlo()
334 334 s.starttls()
335 335 s.ehlo()
336 336
337 337 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
338 338 stored = password = password_store.get_smtp_password(
339 339 mailhost, mailport, username)
340 340 # No need to check whether password was found as try_smtp_login
341 341 # just returns False if it is absent.
342 342 while not try_smtp_login(ui, s, username, password):
343 343 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
344 344
345 345 if stored != password:
346 346 password_store.set_smtp_password(
347 347 mailhost, mailport, username, password)
348 348 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
349 349
350 350 def send(sender, recipients, msg):
351 351 try:
352 352 return s.sendmail(sender, recipients, msg)
353 except smtplib.SMTPRecipientsRefused, inst:
353 except smtplib.SMTPRecipientsRefused as inst:
354 354 recipients = [r[1] for r in inst.recipients.values()]
355 355 raise util.Abort('\n' + '\n'.join(recipients))
356 except smtplib.SMTPException, inst:
356 except smtplib.SMTPException as inst:
357 357 raise util.Abort(inst)
358 358
359 359 return send
360 360
361 361 ############################################################
362 362
363 363 orig_smtp = mail._smtp
364 364
365 365 @monkeypatch_method(mail)
366 366 def _smtp(ui):
367 367 """
368 368 build an smtp connection and return a function to send email
369 369
370 370 This is the monkeypatched version of _smtp(ui) function from
371 371 mercurial/mail.py. It calls the original unless username
372 372 without password is given in the configuration.
373 373 """
374 374 username = ui.config('smtp', 'username')
375 375 password = ui.config('smtp', 'password')
376 376
377 377 if username and not password:
378 378 return keyring_supported_smtp(ui, username)
379 379 else:
380 380 return orig_smtp(ui)
General Comments 0
You need to be logged in to leave comments. Login now