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