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