##// END OF EJS Templates
pypi install option documented
Marcin Kasperski -
r28:eec03adc default
parent child Browse files
Show More
@@ -1,480 +1,495 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 """
11 11 =================
12 12 mercurial_keyring
13 13 =================
14 14
15 15 Mercurial extension to securely save HTTP and SMTP authentication
16 16 passwords in password databases (Gnome Keyring, KDE KWallet,
17 17 OSXKeyChain, specific solutions for Win32 and command line). Uses and
18 18 wraps services of the keyring_ library.
19 19
20 20 .. _keyring: http://pypi.python.org/pypi/keyring
21 21
22 22 How does it work
23 23 ================
24 24
25 25 The extension prompts for the password on the first pull/push (in case
26 26 of HTTP) or first email (in case of SMTP), just like it is done by
27 27 default, but saves the given password (keyed by the combination of
28 28 username and remote repository url - for HTTP - or smtp server
29 29 address - for SMTP) in the password database. On successive runs it
30 30 checks for the username in ``.hg/hgrc``, then for suitable password in the
31 31 password database, and uses those credentials if found.
32 32
33 33 In case password turns out to be incorrect (either because it was
34 34 invalid, or because it was changed on the server) it just prompts the
35 35 user again.
36 36
37 37 Installation
38 38 ============
39 39
40 40 Install keyring library:
41 41
42 42 ::
43 43
44 44 easy_install keyring
45 45
46 46 (or ``pip keyring``). On Debian "Sid" the library can be also
47 47 installed from the official archive (packages ``python-keyring``,
48 48 ``python-keyring-gnome`` and ``python-keyring-kwallet``).
49 49
50 Then either save ``mercurial_keyring.py`` anywhere and put the
51 following in ~/.hgrc (or /etc/mercurial/hgrc):
50 Then use one of the three options:
51
52 a) download ``mercurial_keyring.py``, save it anywhere you like and
53 put the following in ``~/.hgrc`` (or ``/etc/mercurial/hgrc``):
52 54
53 55 ::
54 56
55 57 [extensions]
56 58 hgext.mercurial_keyring = /path/to/mercurial_keyring.py
57 59
58 or save ``mercurial_keyring.py`` to ``mercurial/hgext`` directory and
60 b) save ``mercurial_keyring.py`` to ``mercurial/hgext`` directory and
59 61 use
60 62
61 63 ::
62 64
63 65 [extensions]
64 66 hgext.mercurial_keyring =
65 67
68 c) install ``mercurial_keyring`` using ``easy_install``:
69
70 ::
71
72 easy_install mercurial_keyring
73
74 and then configure ``~/.hgrc`` so:
75
76 ::
77
78 [extensions]
79 mercurial_keyring =
80
66 81 Password backend configuration
67 82 ==============================
68 83
69 84 The library should usually pick the most appropriate password backend
70 85 without configuration. Still, if necessary, it can be configured using
71 86 ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
72 87 the current user). Refer to keyring_ docs for more details.
73 88
74 89 ''I considered handling similar options in hgrc, but decided that
75 90 single user may use more than one keyring-based script. Still, I am
76 91 open to suggestions.''
77 92
78 93 Repository configuration (HTTP)
79 94 ===============================
80 95
81 96 Edit repository-local ``.hg/hgrc`` and save there the remote
82 97 repository path and the username, but do not save the password. For
83 98 example:
84 99
85 100 ::
86 101
87 102 [paths]
88 103 myremote = https://my.server.com/hgrepo/someproject
89 104
90 105 [auth]
91 106 myremote.schemes = http https
92 107 myremote.prefix = my.server.com/hgrepo
93 108 myremote.username = mekk
94 109
95 110 Simpler form with url-embedded name can also be used:
96 111
97 112 ::
98 113
99 114 [paths]
100 115 bitbucket = https://User@bitbucket.org/User/project_name/
101 116
102 117 Note: if both username and password are given in ``.hg/hgrc``,
103 118 extension will use them without using the password database. If
104 119 username is not given, extension will prompt for credentials every
105 120 time, also without saving the password.
106 121
107 122 Repository configuration (SMTP)
108 123 ===============================
109 124
110 125 Edit either repository-local ``.hg/hgrc``, or ``~/.hgrc`` and set
111 126 there all standard email and smtp properties, including smtp
112 127 username, but without smtp password. For example:
113 128
114 129 ::
115 130
116 131 [email]
117 132 method = smtp
118 133 from = Joe Doe <Joe.Doe@remote.com>
119 134
120 135 [smtp]
121 136 host = smtp.gmail.com
122 137 port = 587
123 138 username = JoeDoe@gmail.com
124 139 tls = true
125 140
126 141 Just as in case of HTTP, you *must* set username, but *must not* set
127 142 password here to use the extension, in other cases it will revert to
128 143 the default behaviour.
129 144
130 145 Usage
131 146 =====
132 147
133 148 Configure the repository as above, then just pull, push, etc.
134 149 You should be asked for the password only once (per every
135 150 username+remote_repository_url combination).
136 151
137 152 Similarly, for email, configure as above and just email.
138 153 Again, you will be asked for the password once (per every
139 154 username+email_server_name+email_server_port).
140 155
141 156 Implementation details
142 157 ======================
143 158
144 159 The extension is monkey-patching the mercurial passwordmgr class to
145 160 replace the find_user_password method. Detailed order of operations
146 161 is described in the comments inside the code.
147 162
148 163 """
149 164
150 165 from mercurial import hg, repo, util
151 166 from mercurial.i18n import _
152 167 try:
153 168 from mercurial.url import passwordmgr
154 169 except:
155 170 from mercurial.httprepo import passwordmgr
156 171 from mercurial.httprepo import httprepository
157 172 from mercurial import mail
158 173
159 174 import keyring
160 175 from urlparse import urlparse
161 176 import urllib2
162 177 import smtplib, socket
163 178
164 179 KEYRING_SERVICE = "Mercurial"
165 180
166 181 ############################################################
167 182
168 183 def monkeypatch_method(cls):
169 184 def decorator(func):
170 185 setattr(cls, func.__name__, func)
171 186 return func
172 187 return decorator
173 188
174 189 ############################################################
175 190
176 191 class PasswordStore(object):
177 192 """
178 193 Helper object handling keyring usage (password save&restore,
179 194 the way passwords are keyed in the keyring).
180 195 """
181 196 def __init__(self):
182 197 self.cache = dict()
183 198 def get_http_password(self, url, username):
184 199 return keyring.get_password(KEYRING_SERVICE,
185 200 self._format_http_key(url, username))
186 201 def set_http_password(self, url, username, password):
187 202 keyring.set_password(KEYRING_SERVICE,
188 203 self._format_http_key(url, username),
189 204 password)
190 205 def clear_http_password(self, url, username):
191 206 self.set_http_password(url, username, "")
192 207 def _format_http_key(self, url, username):
193 208 return "%s@@%s" % (username, url)
194 209 def get_smtp_password(self, machine, port, username):
195 210 return keyring.get_password(
196 211 KEYRING_SERVICE,
197 212 self._format_smtp_key(machine, port, username))
198 213 def set_smtp_password(self, machine, port, username, password):
199 214 keyring.set_password(
200 215 KEYRING_SERVICE,
201 216 self._format_smtp_key(machine, port, username),
202 217 password)
203 218 def clear_smtp_password(self, machine, port, username):
204 219 self.set_smtp_password(url, username, "")
205 220 def _format_smtp_key(self, machine, port, username):
206 221 return "%s@@%s:%s" % (username, machine, str(port))
207 222
208 223 password_store = PasswordStore()
209 224
210 225 ############################################################
211 226
212 227 class HTTPPasswordHandler(object):
213 228 """
214 229 Actual implementation of password handling (user prompting,
215 230 configuration file searching, keyring save&restore).
216 231
217 232 Object of this class is bound as passwordmgr attribute.
218 233 """
219 234 def __init__(self):
220 235 self.pwd_cache = {}
221 236 self.last_reply = None
222 237
223 238 def find_auth(self, pwmgr, realm, authuri):
224 239 """
225 240 Actual implementation of find_user_password - different
226 241 ways of obtaining the username and password.
227 242 """
228 243 ui = pwmgr.ui
229 244
230 245 # If we are called again just after identical previous
231 246 # request, then the previously returned auth must have been
232 247 # wrong. So we note this to force password prompt (and avoid
233 248 # reusing bad password indifinitely).
234 249 after_bad_auth = (self.last_reply \
235 250 and (self.last_reply['realm'] == realm) \
236 251 and (self.last_reply['authuri'] == authuri))
237 252
238 253 # Strip arguments to get actual remote repository url.
239 254 base_url = self.canonical_url(authuri)
240 255
241 256 # Extracting possible username (or password)
242 257 # stored directly in repository url
243 258 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
244 259 pwmgr, realm, authuri)
245 260 if user and pwd:
246 261 self._debug_reply(ui, _("Auth data found in repository URL"),
247 262 base_url, user, pwd)
248 263 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
249 264 return user, pwd
250 265
251 266 # Checking the memory cache (there may be many http calls per command)
252 267 cache_key = (realm, base_url)
253 268 if not after_bad_auth:
254 269 cached_auth = self.pwd_cache.get(cache_key)
255 270 if cached_auth:
256 271 user, pwd = cached_auth
257 272 self._debug_reply(ui, _("Cached auth data found"),
258 273 base_url, user, pwd)
259 274 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
260 275 return user, pwd
261 276
262 277 # Loading username and maybe password from [auth] in .hg/hgrc
263 278 nuser, pwd = self.load_hgrc_auth(ui, base_url)
264 279 if nuser:
265 280 if user:
266 281 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, nuser)))
267 282 user = nuser
268 283 if pwd:
269 284 self.pwd_cache[cache_key] = user, pwd
270 285 self._debug_reply(ui, _("Auth data set in .hg/hgrc"),
271 286 base_url, user, pwd)
272 287 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
273 288 return user, pwd
274 289 else:
275 290 ui.debug(_("Username found in .hg/hgrc: %s\n" % user))
276 291
277 292 # Loading password from keyring.
278 293 # Only if username is known (so we know the key) and we are
279 294 # not after failure (so we don't reuse the bad password).
280 295 if user and not after_bad_auth:
281 296 pwd = password_store.get_http_password(base_url, user)
282 297 if pwd:
283 298 self.pwd_cache[cache_key] = user, pwd
284 299 self._debug_reply(ui, _("Keyring password found"),
285 300 base_url, user, pwd)
286 301 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
287 302 return user, pwd
288 303
289 304 # Is the username permanently set?
290 305 fixed_user = (user and True or False)
291 306
292 307 # Last resort: interactive prompt
293 308 if not ui.interactive():
294 309 raise util.Abort(_('mercurial_keyring: http authorization required'))
295 310 ui.write(_("http authorization required\n"))
296 311 ui.status(_("realm: %s\n") % realm)
297 312 if fixed_user:
298 313 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
299 314 else:
300 315 user = ui.prompt(_("user:"), default=None)
301 316 pwd = ui.getpass(_("password: "))
302 317
303 318 if fixed_user:
304 319 # Saving password to the keyring.
305 320 # It is done only if username is permanently set.
306 321 # Otherwise we won't be able to find the password so it
307 322 # does not make much sense to preserve it
308 323 ui.debug("Saving password for %s to keyring\n" % user)
309 324 password_store.set_http_password(base_url, user, pwd)
310 325
311 326 # Saving password to the memory cache
312 327 self.pwd_cache[cache_key] = user, pwd
313 328
314 329 self._debug_reply(ui, _("Manually entered password"),
315 330 base_url, user, pwd)
316 331 self.last_reply = dict(realm=realm,authuri=authuri,user=user)
317 332 return user, pwd
318 333
319 334 def load_hgrc_auth(self, ui, base_url):
320 335 """
321 336 Loading username and possibly password from [auth] in local
322 337 repo .hgrc
323 338 """
324 339 # Theoretically 3 lines below should do:
325 340
326 341 #auth_token = self.readauthtoken(base_url)
327 342 #if auth_token:
328 343 # user, pwd = auth.get('username'), auth.get('password')
329 344
330 345 # Unfortunately they do not work, readauthtoken always return
331 346 # None. Why? Because ui (self.ui of passwordmgr) describes the
332 347 # *remote* repository, so does *not* contain any option from
333 348 # local .hg/hgrc.
334 349
335 350 # Workaround: we recreate the repository object
336 351 repo_root = ui.config("bundle", "mainreporoot")
337 352 if repo_root:
338 353 from mercurial.ui import ui as _ui
339 354 import os
340 355 local_ui = _ui(ui)
341 356 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
342 357 local_passwordmgr = passwordmgr(local_ui)
343 358 auth_token = local_passwordmgr.readauthtoken(base_url)
344 359 if auth_token:
345 360 return auth_token.get('username'), auth_token.get('password')
346 361 return None, None
347 362
348 363 def canonical_url(self, authuri):
349 364 """
350 365 Strips query params from url. Used to convert urls like
351 366 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
352 367 to
353 368 https://repo.machine.com/repos/apps/module
354 369 """
355 370 parsed_url = urlparse(authuri)
356 371 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
357 372 parsed_url.path)
358 373
359 374 def _debug_reply(self, ui, msg, url, user, pwd):
360 375 ui.debug("%s. Url: %s, user: %s, passwd: %s\n" % (
361 376 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
362 377
363 378 ############################################################
364 379
365 380 @monkeypatch_method(passwordmgr)
366 381 def find_user_password(self, realm, authuri):
367 382 """
368 383 keyring-based implementation of username/password query
369 384 for HTTP(S) connections
370 385
371 386 Passwords are saved in gnome keyring, OSX/Chain or other platform
372 387 specific storage and keyed by the repository url
373 388 """
374 389 # Extend object attributes
375 390 if not hasattr(self, '_pwd_handler'):
376 391 self._pwd_handler = HTTPPasswordHandler()
377 392
378 393 return self._pwd_handler.find_auth(self, realm, authuri)
379 394
380 395 ############################################################
381 396
382 397 def try_smtp_login(ui, smtp_obj, username, password):
383 398 """
384 399 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
385 400 password.
386 401
387 402 Returns:
388 403 - True if login succeeded
389 404 - False if login failed due to the wrong credentials
390 405
391 406 Throws Abort exception if login failed for any other reason.
392 407
393 408 Immediately returns False if password is empty
394 409 """
395 410 if not password:
396 411 return False
397 412 try:
398 413 ui.note(_('(authenticating to mail server as %s)\n') %
399 414 (username))
400 415 smtp_obj.login(username, password)
401 416 return True
402 417 except smtplib.SMTPException, inst:
403 418 if inst.smtp_code == 535:
404 419 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
405 420 return False
406 421 else:
407 422 raise util.Abort(inst)
408 423
409 424 def keyring_supported_smtp(ui, username):
410 425 """
411 426 keyring-integrated replacement for mercurial.mail._smtp
412 427 Used only when configuration file contains username, but
413 428 does not contain the password.
414 429
415 430 Most of the routine below is copied as-is from
416 431 mercurial.mail._smtp. The only changed part is
417 432 marked with #>>>>> and #<<<<< markers
418 433 """
419 434 local_hostname = ui.config('smtp', 'local_hostname')
420 435 s = smtplib.SMTP(local_hostname=local_hostname)
421 436 mailhost = ui.config('smtp', 'host')
422 437 if not mailhost:
423 438 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
424 439 mailport = int(ui.config('smtp', 'port', 25))
425 440 ui.note(_('sending mail: smtp host %s, port %s\n') %
426 441 (mailhost, mailport))
427 442 s.connect(host=mailhost, port=mailport)
428 443 if ui.configbool('smtp', 'tls'):
429 444 if not hasattr(socket, 'ssl'):
430 445 raise util.Abort(_("can't use TLS: Python SSL support "
431 446 "not installed"))
432 447 ui.note(_('(using tls)\n'))
433 448 s.ehlo()
434 449 s.starttls()
435 450 s.ehlo()
436 451
437 452 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
438 453 stored = password = password_store.get_smtp_password(
439 454 mailhost, mailport, username)
440 455 # No need to check whether password was found as try_smtp_login
441 456 # just returns False if it is absent.
442 457 while not try_smtp_login(ui, s, username, password):
443 458 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
444 459
445 460 if stored != password:
446 461 password_store.set_smtp_password(
447 462 mailhost, mailport, username, password)
448 463 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
449 464
450 465 def send(sender, recipients, msg):
451 466 try:
452 467 return s.sendmail(sender, recipients, msg)
453 468 except smtplib.SMTPRecipientsRefused, inst:
454 469 recipients = [r[1] for r in inst.recipients.values()]
455 470 raise util.Abort('\n' + '\n'.join(recipients))
456 471 except smtplib.SMTPException, inst:
457 472 raise util.Abort(inst)
458 473
459 474 return send
460 475
461 476 ############################################################
462 477
463 478 orig_smtp = mail._smtp
464 479
465 480 @monkeypatch_method(mail)
466 481 def _smtp(ui):
467 482 """
468 483 build an smtp connection and return a function to send email
469 484
470 485 This is the monkeypatched version of _smtp(ui) function from
471 486 mercurial/mail.py. It calls the original unless username
472 487 without password is given in the configuration.
473 488 """
474 489 username = ui.config('smtp', 'username')
475 490 password = ui.config('smtp', 'password')
476 491
477 492 if username and not password:
478 493 return keyring_supported_smtp(ui, username)
479 494 else:
480 495 return orig_smtp(ui)
General Comments 0
You need to be logged in to leave comments. Login now